aves_mio1/lib/widgets/home/home_page.dart.old
FabioMich66 deb7b4c6dd
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
super
2026-04-10 10:07:04 +02:00

972 lines
35 KiB
Dart

// lib/widgets/home/home_page.dart
import 'dart:async';
import 'package:aves/remote/collection_source_remote_ext.dart';
import 'package:aves/app_mode.dart';
import 'package:aves/geo/uri.dart';
import 'package:aves/model/app/intent.dart';
import 'package:aves/model/app/permissions.dart';
import 'package:aves/model/app_inventory.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/analysis_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/global_search.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/services/widget_service.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/search/page.dart';
import 'package:aves/widgets/common/search/route.dart';
import 'package:aves/widgets/editor/entry_editor_page.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/home/home_error.dart';
import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/search/collection_search_delegate.dart';
import 'package:aves/widgets/settings/home_widget_settings_page.dart';
import 'package:aves/widgets/settings/screen_saver_settings_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/screen_saver_page.dart';
import 'package:aves/widgets/wallpaper_page.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
// --- REMOTO / DEBUG ---
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'package:aves/remote/remote_test_page.dart' as rtp;
import 'package:aves/remote/remote_settings.dart';
import 'package:aves/remote/remote_http.dart';
import 'package:aves/remote/remote_models.dart';
import 'package:aves/remote/remote_client.dart';
import 'package:aves/remote/auth_client.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// Step 2: progress bus + repository
import 'package:aves/remote/remote_sync_bus.dart';
import 'package:aves/remote/remote_repository.dart';
class HomePage extends StatefulWidget {
static const routeName = '/';
// untyped map as it is coming from the platform
final Map? intentData;
const HomePage({
super.key,
this.intentData,
});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
AvesEntry? _viewerEntry;
int? _widgetId;
String? _initialRouteName, _initialSearchQuery;
Set<CollectionFilter>? _initialFilters;
String? _initialExplorerPath;
(LatLng, double?)? _initialLocationZoom;
List<String>? _secureUris;
(Object, StackTrace)? _setupError;
// guard sync remoto: singola esecuzione per avvio
bool _remoteSyncScheduled = false;
bool _remoteSyncActive = false;
// guard pagina test remota
bool _remoteTestOpen = false;
static const allowedShortcutRoutes = [
AlbumListPage.routeName,
CollectionPage.routeName,
ExplorerPage.routeName,
MapPage.routeName,
SearchPage.routeName,
];
@override
void initState() {
super.initState();
_setup();
imageCache.maximumSizeBytes = 512 * (1 << 20);
}
@override
Widget build(BuildContext context) => AvesScaffold(
body: _setupError != null
? HomeError(
error: _setupError!.$1,
stack: _setupError!.$2,
)
: null,
);
// ============================================================
// BOOTSTRAP FLAG (Remote progress ONLY first time)
// ============================================================
Future<bool> _isRemoteBootstrapDone() async {
final storage = FlutterSecureStorage();
final v = await storage.read(key: 'remote_bootstrap_done');
return v == '1';
}
Future<void> _setRemoteBootstrapDone() async {
final storage = FlutterSecureStorage();
await storage.write(key: 'remote_bootstrap_done', value: '1');
}
// ============================================================
Future<void> _setup() async {
try {
final stopwatch = Stopwatch()..start();
if (await windowService.isActivity()) {
await Permissions.mediaAccess.request();
}
var appMode = AppMode.main;
var error = false;
final intentData = widget.intentData ?? await IntentService.getIntentData();
final intentAction = intentData[IntentDataKeys.action] as String?;
_initialFilters = null;
_initialExplorerPath = null;
_secureUris = null;
await availability.onNewIntent();
await androidFileUtils.init();
// Warm-up header remoti (non blocca UI)
unawaited(Future(() async {
try {
final s = await _safeLoadRemoteSettings();
if (s.enabled && s.baseUrl.trim().isNotEmpty) {
await _safeHeaders();
}
} catch (_) {}
}));
if (!{
IntentActions.edit,
IntentActions.screenSaver,
IntentActions.setWallpaper,
}.contains(intentAction) &&
settings.isInstalledAppAccessAllowed) {
unawaited(appInventory.initAppNames());
}
if (intentData.values.nonNulls.isNotEmpty) {
await reportService.log('Intent data=$intentData');
var intentUri = intentData[IntentDataKeys.uri] as String?;
final intentMimeType = intentData[IntentDataKeys.mimeType] as String?;
switch (intentAction) {
case IntentActions.view:
appMode = AppMode.view;
_secureUris = (intentData[IntentDataKeys.secureUris] as List?)?.cast<String>();
case IntentActions.viewGeo:
error = true;
if (intentUri != null) {
final locationZoom = parseGeoUri(intentUri);
if (locationZoom != null) {
_initialRouteName = MapPage.routeName;
_initialLocationZoom = locationZoom;
error = false;
}
}
break;
case IntentActions.edit:
appMode = AppMode.edit;
case IntentActions.setWallpaper:
appMode = AppMode.setWallpaper;
case IntentActions.pickItems:
final multiple = (intentData[IntentDataKeys.allowMultiple] as bool?) ?? false;
debugPrint('pick mimeType=$intentMimeType multiple=$multiple');
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
case IntentActions.pickCollectionFilters:
appMode = AppMode.pickCollectionFiltersExternal;
case IntentActions.screenSaver:
appMode = AppMode.screenSaver;
_initialRouteName = ScreenSaverPage.routeName;
case IntentActions.screenSaverSettings:
_initialRouteName = ScreenSaverSettingsPage.routeName;
case IntentActions.search:
_initialRouteName = SearchPage.routeName;
_initialSearchQuery = intentData[IntentDataKeys.query] as String?;
case IntentActions.widgetSettings:
_initialRouteName = HomeWidgetSettingsPage.routeName;
_widgetId = (intentData[IntentDataKeys.widgetId] as int?) ?? 0;
case IntentActions.widgetOpen:
final widgetId = intentData[IntentDataKeys.widgetId] as int?;
if (widgetId == null) {
error = true;
} else {
await settings.reload();
final page = settings.getWidgetOpenPage(widgetId);
switch (page) {
case WidgetOpenPage.collection:
_initialFilters = settings.getWidgetCollectionFilters(widgetId);
case WidgetOpenPage.viewer:
appMode = AppMode.view;
intentUri = settings.getWidgetUri(widgetId);
case WidgetOpenPage.home:
case WidgetOpenPage.updateWidget:
break;
}
unawaited(WidgetService.update(widgetId));
}
default:
final extraRoute = intentData[IntentDataKeys.page] as String?;
if (allowedShortcutRoutes.contains(extraRoute)) {
_initialRouteName = extraRoute;
}
}
if (_initialFilters == null) {
final extraFilters = (intentData[IntentDataKeys.filters] as List?)?.cast<String>();
_initialFilters = extraFilters?.map(CollectionFilter.fromJson).nonNulls.toSet();
}
_initialExplorerPath = intentData[IntentDataKeys.explorerPath] as String?;
switch (appMode) {
case AppMode.view:
case AppMode.edit:
case AppMode.setWallpaper:
if (intentUri != null) {
_viewerEntry = await _initViewerEntry(
uri: intentUri,
mimeType: intentMimeType,
);
}
error = _viewerEntry == null;
default:
break;
}
}
if (error) {
debugPrint('Failed to init app mode=$appMode for intent data=$intentData. Fallback to main mode.');
appMode = AppMode.main;
}
context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
switch (appMode) {
case AppMode.main:
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickSingleMediaExternal:
case AppMode.pickMultipleMediaExternal:
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// STEP 1 STARTUP VELOCE (MARKER 1/6) - NON MODIFICARE FUORI DA QUESTI MARKER
// STEP 1 STARTUP VELOCE (MARKER 2/6) - DB cache -> init in background
// STEP 1 STARTUP VELOCE (MARKER 3/6) - DB vuoto -> init standard con progress
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// Capisco se c'è cache nel DB (locali e/o remoti)
bool hasAnyCache = false;
try {
await localMediaDb.init(); // assicura DB pronto
final rows = await localMediaDb.rawDb.rawQuery(
'SELECT 1 FROM entry WHERE trashed=0 LIMIT 1',
);
hasAnyCache = rows.isNotEmpty;
} catch (_) {}
final loadTopEntriesFirst =
settings.homeNavItem.route == CollectionPage.routeName &&
settings.homeCustomCollection.isEmpty;
// Bootstrap flag remoti (progress SOLO prima volta)
final bootstrapDone = await _isRemoteBootstrapDone();
final bootstrap = !bootstrapDone;
// Se la source non è full scope, dobbiamo fare init almeno una volta
if (source.loadedScope != CollectionSource.fullScope) {
await reportService.log(
'Initialize source to start app with mode=$appMode, loaded scope=${source.loadedScope}',
);
if (hasAnyCache) {
// ✅ DB ha dati: avvio veloce -> init in background (non blocca UI)
debugPrint('[startup] DB cache present -> init in background (fast start)');
source.canAnalyze = true;
unawaited(
source
.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst)
.then((_) async {
// ✅ Remoti: opzione 1
// - se bootstrap DONE -> mostra subito dal DB
// - se bootstrap NOT done -> NON mostrare finché non finisce il bootstrap sync
final bd = await _isRemoteBootstrapDone();
if (bd) {
await source.appendRemoteEntriesFromDb();
debugPrint('[startup][bg] remote append after init done');
} else {
debugPrint('[startup][bg] bootstrap not done -> skip remote append (will appear after bootstrap sync)');
}
// Schedula sync remoto UNA volta
if (!_remoteSyncScheduled) {
_remoteSyncScheduled = true;
final sourceRef = source;
unawaited(Future.microtask(() => _runRemoteSync(sourceRef, bootstrap: !bd)));
}
}),
);
} else {
// ✅ DB vuoto: comportamento Aves standard -> await init (progress locale)
debugPrint('[startup] DB empty -> await init (Aves standard)');
source.canAnalyze = true;
await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst);
// ✅ Remoti: opzione 1
// - se bootstrap DONE -> mostra subito dal DB
// - se bootstrap NOT done -> non mostrare finché bootstrap sync finisce
if (!bootstrap) {
// bootstrap==true => primo avvio remoto
debugPrint('[startup] bootstrap not done -> skip remote append (will appear after bootstrap sync)');
} else {
await source.appendRemoteEntriesFromDb();
}
if (!_remoteSyncScheduled) {
_remoteSyncScheduled = true;
final sourceRef = source;
unawaited(Future.microtask(() => _runRemoteSync(sourceRef, bootstrap: bootstrap)));
}
}
} else {
// Source già full scope (hot state)
debugPrint('[startup] source already fullScope');
// ✅ Remoti: opzione 1
// se bootstrap done -> mostra subito dal DB
if (bootstrapDone) {
await source.appendRemoteEntriesFromDb();
} else {
debugPrint('[startup] bootstrap not done -> skip remote append (will appear after bootstrap sync)');
}
if (!_remoteSyncScheduled) {
_remoteSyncScheduled = true;
final sourceRef = source;
unawaited(Future.microtask(() => _runRemoteSync(sourceRef, bootstrap: bootstrap)));
}
}
// DIAG: stato (facoltativo)
unawaited(_printRemoteDiag(source, when: ' PRE'));
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// STEP 1 STARTUP VELOCE (MARKER 4/6) - FINE BLOCCO
// STEP 1 STARTUP VELOCE (MARKER 5/6) - QUI PUOI AGGIUNGERE LOG/DIAG
// STEP 1 STARTUP VELOCE (MARKER 6/6) - NON MODIFICARE FUORI DA QUESTI MARKER
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
break;
case AppMode.screenSaver:
await reportService.log('Initialize source to start screen saver');
final source2 = context.read<CollectionSource>();
source2.canAnalyze = false;
await source2.init(scope: settings.screenSaverCollectionFilters);
break;
case AppMode.view:
if (_isViewerSourceable(_viewerEntry) && _secureUris == null) {
final directory = _viewerEntry?.directory;
if (directory != null) {
unawaited(AnalysisService.registerCallback());
await reportService.log('Initialize source to view item in directory $directory');
final source = context.read<CollectionSource>();
source.canAnalyze = true;
await source.init(scope: {StoredAlbumFilter(directory, null)});
}
} else {
await _initViewerEssentials();
}
break;
case AppMode.edit:
case AppMode.setWallpaper:
await _initViewerEssentials();
break;
default:
break;
}
debugPrint('Home setup complete in ${stopwatch.elapsed.inMilliseconds}ms');
// navigazione finale
unawaited(
Navigator.maybeOf(context)?.pushAndRemoveUntil(
await _getRedirectRoute(appMode),
(route) => false,
),
);
} catch (error, stack) {
debugPrint('failed to setup app with error=$error\n$stack');
setState(() => _setupError = (error, stack));
}
}
// ============================================================
// === SYNC REMOTO (Step 2)
// - progress bar SOLO bootstrap (prima volta)
// - remoti visibili SOLO dopo bootstrap completato
// - dopo bootstrap: niente full sync automatico (fino a Step 3 delta/ws)
// ============================================================
Future<void> _runRemoteSync(CollectionSource source, {required bool bootstrap}) async {
try {
final rs = await _safeLoadRemoteSettings();
if (!rs.enabled) {
debugPrint('[remote-sync] disabled → skip');
return;
}
// Se NON bootstrap: per ora non facciamo full fetch ogni avvio
if (!bootstrap) {
debugPrint('[remote-sync] bootstrap already done -> skip full sync (until Step 3 delta/ws)');
return;
}
// Se locali ancora in loading, attendi
try {
if (source.stateNotifier.value == SourceState.loading) {
final c = Completer<void>();
void onState() {
if (source.stateNotifier.value != SourceState.loading) {
source.stateNotifier.removeListener(onState);
c.complete();
}
}
source.stateNotifier.addListener(onState);
onState();
await c.future;
}
} catch (_) {}
_remoteSyncActive = true;
// FULL fetch dal server (bootstrap)
final items = await _fetchAllRemoteItems();
final total = items.length;
final serverIds = items.map((e) => e.id).where((v) => v.isNotEmpty).toSet();
// progress start SOLO bootstrap
RemoteSyncBus.instance.start(phase: 'Sync remoto…', total: total);
final repo = RemoteRepository(localMediaDb.rawDb);
// Bootstrap: pulizia totale remoti prima di importare
await repo.deleteAllRemotes();
// upsert chunked con progress reale X/Y
const chunkSize = 200;
int done = 0;
for (var offset = 0; offset < total; offset += chunkSize) {
final end = (offset + chunkSize < total) ? offset + chunkSize : total;
final chunk = items.sublist(offset, end);
await repo.upsertAll(chunk, chunkSize: chunkSize);
done = end;
RemoteSyncBus.instance.update(
phase: 'Sync remoto…',
done: done,
total: total,
);
}
// prune hard-delete (full list autorevole)
final pruned = await repo.pruneMissingRemotes(serverIds);
debugPrint('[remote-sync] prune deleted=$pruned');
// ✅ Remoti compaiono SOLO ORA (dopo caricamento completo)
await source.appendRemoteEntriesFromDb();
// segna bootstrap done
await _setRemoteBootstrapDone();
RemoteSyncBus.instance.finish();
unawaited(_printRemoteDiag(source, when: ' POST'));
} on TimeoutException catch (e) {
debugPrint('[remote-sync] TIMEOUT: $e');
RemoteSyncBus.instance.clear();
} catch (e, st) {
debugPrint('[remote-sync] error: $e\n$st');
RemoteSyncBus.instance.clear();
} finally {
_remoteSyncActive = false;
}
}
// === FETCH remoto reale ===
Future<List<RemotePhotoItem>> _fetchAllRemoteItems() async {
try {
final rs = await _safeLoadRemoteSettings();
if (!rs.enabled || rs.baseUrl.trim().isEmpty) {
return <RemotePhotoItem>[];
}
RemoteAuth? auth;
if (rs.email.isNotEmpty && rs.password.isNotEmpty) {
auth = RemoteAuth(baseUrl: rs.baseUrl, email: rs.email, password: rs.password);
}
final client = RemoteJsonClient(rs.baseUrl, rs.indexPath, auth: auth);
try {
final items = await client.fetchAll();
debugPrint('[remote-sync][fetch] fetched ${items.length} items');
return items;
} catch (e, st) {
debugPrint('[remote-sync][fetch] client.fetchAll ERROR: $e\n$st');
return <RemotePhotoItem>[];
}
} catch (e, st) {
debugPrint('[remote-sync][fetch] ERROR: $e\n$st');
return <RemotePhotoItem>[];
}
}
// --- DIAGNOSTICA ---
Future<void> _printRemoteDiag(CollectionSource source, {String when = ''}) async {
try {
final dbRem = await localMediaDb.loadEntries(origin: 1);
final dbCount = dbRem.length;
final displayable = dbRem.where((e) => !e.trashed && e.isDisplayable).length;
final inSource = source.allEntries.where((e) => e.origin == 1 && !e.trashed).length;
final inVisible = source.visibleEntries.where((e) => e.origin == 1 && !e.trashed).length;
debugPrint('[diag$when] DB remoti=$dbCount, displayable=$displayable, inSource=$inSource, inVisible=$inVisible');
} catch (e, st) {
debugPrint('[diag$when] ERROR: $e\n$st');
}
}
Future<void> _initViewerEssentials() async {
await localMediaDb.init();
}
bool _isViewerSourceable(AvesEntry? viewerEntry) {
return viewerEntry != null &&
viewerEntry.directory != null &&
!settings.hiddenFilters.any((filter) => filter.test(viewerEntry));
}
Future<AvesEntry?> _initViewerEntry({required String uri, required String? mimeType}) async {
if (uri.startsWith('/')) {
uri = Uri.file(uri).toString();
}
final entry = await mediaFetchService.getEntry(uri, mimeType);
if (entry != null) {
await entry.catalog(background: false, force: false, persist: false);
}
return entry;
}
// === DEBUG: pagina test remoto con DB indipendente ===
Future<void> _openRemoteTestPage(BuildContext context) async {
if (_remoteTestOpen) return;
if (_remoteSyncActive) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Attendi fine sync remoto in corso prima di aprire RemoteTest')),
);
return;
}
_remoteTestOpen = true;
Database? debugDb;
try {
final dbDir = await getDatabasesPath();
final dbPath = p.join(dbDir, 'metadata.db');
debugDb = await openDatabase(
dbPath,
singleInstance: false,
onConfigure: (db) async {
await db.rawQuery('PRAGMA journal_mode=WAL');
await db.rawQuery('PRAGMA foreign_keys=ON');
},
);
if (!context.mounted) return;
final rs = await _safeLoadRemoteSettings();
final baseUrl = rs.baseUrl.isNotEmpty ? rs.baseUrl : RemoteSettings.defaultBaseUrl;
await Navigator.of(context).push(MaterialPageRoute(
builder: (_) => rtp.RemoteTestPage(
db: debugDb!,
baseUrl: baseUrl,
),
));
} catch (e, st) {
// ignore: avoid_print
print('[RemoteTest] errore apertura DB/pagina: $e\n$st');
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore RemoteTest: $e')),
);
} finally {
try {
await debugDb?.close();
} catch (_) {}
_remoteTestOpen = false;
}
}
// === DEBUG: dialog impostazioni remote (semplice) ===
Future<void> _openRemoteSettingsDialog(BuildContext context) async {
final s = await _safeLoadRemoteSettings();
final formKey = GlobalKey<FormState>();
bool enabled = s.enabled;
final baseUrlC = TextEditingController(text: s.baseUrl);
final indexC = TextEditingController(text: s.indexPath);
final emailC = TextEditingController(text: s.email);
final pwC = TextEditingController(text: s.password);
await showDialog<void>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Remote Settings'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SwitchListTile(
title: const Text('Abilita sync remoto'),
value: enabled,
onChanged: (v) {
enabled = v;
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 8),
TextFormField(
controller: baseUrlC,
decoration: const InputDecoration(
labelText: 'Base URL',
hintText: 'https://prova.patachina.it',
),
validator: (v) => (v == null || v.isEmpty) ? 'Obbligatorio' : null,
),
const SizedBox(height: 8),
TextFormField(
controller: indexC,
decoration: const InputDecoration(
labelText: 'Index path',
hintText: 'photos/',
),
validator: (v) => (v == null || v.isEmpty) ? 'Obbligatorio' : null,
),
const SizedBox(height: 8),
TextFormField(
controller: emailC,
decoration: const InputDecoration(labelText: 'User/Email'),
),
const SizedBox(height: 8),
TextFormField(
controller: pwC,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).maybePop(),
child: const Text('Annulla'),
),
ElevatedButton.icon(
onPressed: () async {
if (!formKey.currentState!.validate()) return;
final upd = RemoteSettings(
enabled: enabled,
baseUrl: baseUrlC.text.trim(),
indexPath: indexC.text.trim(),
email: emailC.text.trim(),
password: pwC.text,
);
await upd.save();
await RemoteHttp.refreshFromSettings();
unawaited(RemoteHttp.warmUp());
if (context.mounted) Navigator.of(context).pop();
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Impostazioni salvate')));
}
},
icon: const Icon(Icons.save),
label: const Text('Salva'),
),
],
),
);
baseUrlC.dispose();
indexC.dispose();
emailC.dispose();
pwC.dispose();
}
// --- DEBUG: wrapper che aggiunge 2 FAB (Settings + Remote Test) ---
Widget _wrapWithRemoteDebug(BuildContext context, Widget child) {
if (!kDebugMode) return child;
return Stack(
children: [
child,
Positioned(
right: 16,
bottom: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: 'remote_debug_settings_fab',
mini: true,
onPressed: () => _openRemoteSettingsDialog(context),
tooltip: 'Remote Settings',
child: const Icon(Icons.settings),
),
const SizedBox(height: 12),
FloatingActionButton(
heroTag: 'remote_debug_test_fab',
onPressed: () => _openRemoteTestPage(context),
tooltip: 'Remote Test',
child: const Icon(Icons.image_search),
),
],
),
),
],
);
}
Future<Route> _getRedirectRoute(AppMode appMode) async {
String routeName;
Set<CollectionFilter?>? filters;
switch (appMode) {
case AppMode.setWallpaper:
return DirectMaterialPageRoute(
settings: const RouteSettings(name: WallpaperPage.routeName),
builder: (_) {
return WallpaperPage(entry: _viewerEntry);
},
);
case AppMode.view:
AvesEntry viewerEntry = _viewerEntry!;
CollectionLens? collection;
final source = context.read<CollectionSource>();
final album = viewerEntry.directory;
if (album != null) {
final loadingCompleter = Completer();
final stateNotifier = source.stateNotifier;
void _onSourceStateChanged() {
if (stateNotifier.value != SourceState.loading) {
stateNotifier.removeListener(_onSourceStateChanged);
loadingCompleter.complete();
}
}
stateNotifier.addListener(_onSourceStateChanged);
_onSourceStateChanged();
await loadingCompleter.future;
collection = CollectionLens(
source: source,
filters: {StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album))},
listenToSource: false,
stackBursts: false,
);
final viewerEntryPath = viewerEntry.path;
final collectionEntry =
collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath);
if (collectionEntry != null) {
viewerEntry = collectionEntry;
} else {
debugPrint('collection does not contain viewerEntry=$viewerEntry');
collection = null;
}
}
return DirectMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
builder: (_) => EntryViewerPage(collection: collection, initialEntry: viewerEntry),
);
case AppMode.edit:
return DirectMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
builder: (_) => ImageEditorPage(entry: _viewerEntry!),
);
case AppMode.initialization:
case AppMode.main:
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickSingleMediaExternal:
case AppMode.pickMultipleMediaExternal:
case AppMode.pickFilteredMediaInternal:
case AppMode.pickUnfilteredMediaInternal:
case AppMode.pickFilterInternal:
case AppMode.previewMap:
case AppMode.screenSaver:
case AppMode.slideshow:
routeName = _initialRouteName ?? settings.homeNavItem.route;
filters = _initialFilters ??
(settings.homeNavItem.route == CollectionPage.routeName ? settings.homeCustomCollection : {});
}
Route buildRoute(WidgetBuilder builder) => DirectMaterialPageRoute(
settings: RouteSettings(name: routeName),
builder: builder,
);
final source = context.read<CollectionSource>();
switch (routeName) {
case AlbumListPage.routeName:
return buildRoute((context) => const AlbumListPage(initialGroup: null));
case TagListPage.routeName:
return buildRoute((context) => const TagListPage(initialGroup: null));
case MapPage.routeName:
return buildRoute((context) {
final mapCollection = CollectionLens(
source: source,
filters: {
LocationFilter.located,
if (filters != null) ...filters!,
},
);
return MapPage(
collection: mapCollection,
initialLocation: _initialLocationZoom?.$1,
initialZoom: _initialLocationZoom?.$2,
);
});
case ExplorerPage.routeName:
final path = _initialExplorerPath ?? settings.homeCustomExplorerPath;
return buildRoute((context) => ExplorerPage(path: path));
case HomeWidgetSettingsPage.routeName:
return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!));
case ScreenSaverPage.routeName:
return buildRoute((context) => ScreenSaverPage(source: source));
case ScreenSaverSettingsPage.routeName:
return buildRoute((context) => const ScreenSaverSettingsPage());
case SearchPage.routeName:
return SearchPageRoute(
delegate: CollectionSearchDelegate(
searchFieldLabel: context.l10n.searchCollectionFieldHint,
searchFieldStyle: Themes.searchFieldStyle(context),
source: source,
canPop: false,
initialQuery: _initialSearchQuery,
),
);
case CollectionPage.routeName:
default:
return buildRoute(
(context) => _wrapWithRemoteDebug(
context,
CollectionPage(source: source, filters: filters),
),
);
}
}
// -------------------------
// Utility sicure per remote
// -------------------------
Future<RemoteSettings> _safeLoadRemoteSettings({Duration timeout = const Duration(seconds: 5)}) async {
try {
return await RemoteSettings.load().timeout(timeout);
} catch (e) {
debugPrint('[remote] RemoteSettings.load failed: $e — using defaults');
return RemoteSettings(
enabled: RemoteSettings.defaultEnabled,
baseUrl: RemoteSettings.defaultBaseUrl,
indexPath: RemoteSettings.defaultIndexPath,
email: RemoteSettings.defaultEmail,
password: RemoteSettings.defaultPassword,
);
}
}
Future<Map<String, String>> _safeHeaders({Duration timeout = const Duration(seconds: 6)}) async {
try {
return await RemoteHttp.headers().timeout(timeout);
} catch (e) {
debugPrint('[remote] RemoteHttp.headers failed: $e — returning empty headers');
return const {};
}
}
Future<void> _debugClearRemoteKeys() async {
if (!kDebugMode) return;
try {
final storage = FlutterSecureStorage();
await storage.delete(key: 'remote_base_url');
await storage.delete(key: 'remote_index_path');
await storage.delete(key: 'remote_email');
await storage.delete(key: 'remote_password');
await storage.delete(key: 'remote_enabled');
// utile anche per test bootstrap:
await storage.delete(key: 'remote_bootstrap_done');
debugPrint('[remote] debugClearRemoteKeys executed');
} catch (e) {
debugPrint('[remote] debugClearRemoteKeys failed: $e');
}
}
}