// 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'; // --- IMPORT aggiunti per integrazione remota / telemetria --- 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/run_remote_sync.dart' as rrs; import 'package:aves/remote/remote_settings.dart'; import 'package:aves/remote/remote_http.dart'; // PERF/REMOTE: warm-up headers import 'package:aves/remote/remote_models.dart'; // RemotePhotoItem // --- IMPORT per client reale --- import 'package:aves/remote/remote_client.dart'; import 'package:aves/remote/auth_client.dart'; // secure storage import (used only in debug helper) import 'package:flutter_secure_storage/flutter_secure_storage.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 createState() => _HomePageState(); } class _HomePageState extends State { AvesEntry? _viewerEntry; int? _widgetId; String? _initialRouteName, _initialSearchQuery; Set? _initialFilters; String? _initialExplorerPath; (LatLng, double?)? _initialLocationZoom; List? _secureUris; (Object, StackTrace)? _setupError; // guard UI per schedulare UNA sola run del sync da Home bool _remoteSyncScheduled = false; // indica se il sync è effettivamente in corso bool _remoteSyncActive = false; // guard per evitare doppi push della pagina di 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, ); Future _setup() async { try { final stopwatch = Stopwatch()..start(); if (await windowService.isActivity()) { // do not check whether permission was granted, because some app stores // hide in some countries apps that force quit on permission denial 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(); // PERF/REMOTE: warm-up headers (Bearer) in background — safe version unawaited(Future(() async { try { final s = await _safeLoadRemoteSettings(); if (s.enabled && s.baseUrl.trim().isNotEmpty) { await _safeHeaders(); // popola la cache per peekHeaders() in modo sicuro debugPrint('[startup] remote headers warm-up done (safe)'); } } catch (e) { debugPrint('[startup] remote headers warm-up skipped: $e'); } })); 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(); 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: // some apps define multiple types, separated by a space 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 { // widget settings may be modified in a different process after channel setup 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(); _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>().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(); if (source.loadedScope != CollectionSource.fullScope) { await reportService.log( 'Initialize source to start app with mode=$appMode, loaded scope=${source.loadedScope}', ); final loadTopEntriesFirst = settings.homeNavItem.route == CollectionPage.routeName && settings.homeCustomCollection.isEmpty; // PERF: UI-first → niente analisi prima della prima paint source.canAnalyze = false; final swInit = Stopwatch()..start(); await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst); swInit.stop(); debugPrint('[startup] source.init done in ${swInit.elapsedMilliseconds}ms'); } // REMOTE: aggiungi remoti visibili (origin=1, trashed=0) final swAppend1 = Stopwatch()..start(); await source.appendRemoteEntriesFromDb(); swAppend1.stop(); debugPrint('[startup] appendRemoteEntries (pre-sync) in ${swAppend1.elapsedMilliseconds}ms'); // === DIAGNOSTICA PRE- SYNC === await _printRemoteDiag(source, when: ' PRE'); // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // PATCH A: se ci sono remoti in DB, forza la Collection "All items" try { final remCount = (await localMediaDb.rawDb .rawQuery('SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=0')) .first['c'] as int? ?? 0; if (remCount > 0) { _initialRouteName = CollectionPage.routeName; _initialFilters = {}; // All items (nessun filtro) debugPrint('[startup] forcing CollectionPage All-items (remoti=$remCount)'); } } catch (e) { debugPrint('[startup] unable to count remotes: $e'); } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // PERF: riattiva l’analisi in background appena la UI è pronta unawaited(Future.delayed(const Duration(milliseconds: 300)).then((_) { source.canAnalyze = true; debugPrint('[startup] analysis re-enabled in background'); })); // === SYNC REMOTO post-init (non blocca la UI) === if (!_remoteSyncScheduled) { _remoteSyncScheduled = true; // una sola schedulazione per avvio unawaited(Future(() async { try { await RemoteSettings.debugSeedIfEmpty(); final rs = await _safeLoadRemoteSettings(); if (!rs.enabled) return; // attesa fine loading final notifier = source.stateNotifier; if (notifier.value == SourceState.loading) { final completer = Completer(); void onState() { if (notifier.value != SourceState.loading) { notifier.removeListener(onState); completer.complete(); } } notifier.addListener(onState); // nel caso non sia già loading: onState(); await completer.future; } // piccolo margine per step secondari (tag, ecc.) await Future.delayed(const Duration(milliseconds: 400)); // ⬇️ SYNC su **stessa connessione** + FETCH (obbligatorio) debugPrint('[remote-sync] START (scheduled=$_remoteSyncScheduled)'); _remoteSyncActive = true; try { final swSync = Stopwatch()..start(); final imported = await rrs.runRemoteSyncOnceManaged( fetch: _fetchAllRemoteItems, // 👈 PASSIAMO IL FETCH ).timeout(const Duration(seconds: 60)); // timeout regolabile swSync.stop(); debugPrint('[remote-sync] completed in ${swSync.elapsedMilliseconds}ms, imported=$imported'); } on TimeoutException catch (e) { debugPrint('[remote-sync] TIMEOUT after 60s: $e'); } catch (e, st) { debugPrint('[remote-sync] error: $e\n$st'); } finally { _remoteSyncActive = false; debugPrint('[remote-sync] END (active=$_remoteSyncActive)'); } // REMOTE: dopo il sync, append di eventuali nuovi remoti if (mounted) { final swAppend2 = Stopwatch()..start(); await source.appendRemoteEntriesFromDb(); swAppend2.stop(); debugPrint('[remote-sync] appendRemoteEntries (post-sync) in ${swAppend2.elapsedMilliseconds}ms'); // 🔎 Conteggio di debug usando una CollectionLens temporanea final c = _countRemotesInSource(source); debugPrint('[check] remoti in CollectionSource = $c'); // === DIAGNOSTICA POST- SYNC === await _printRemoteDiag(source, when: ' POST'); } } catch (e, st) { debugPrint('[remote-sync] outer error: $e\n$st'); } })); } break; case AppMode.screenSaver: await reportService.log('Initialize source to start screen saver'); final source2 = context.read(); 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(); // analysis is necessary to display neighbour items when the initial item is a new one 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'); // `pushReplacement` is not enough in some edge cases // e.g. when opening the viewer in `view` mode should replace a viewer in `main` mode 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)); } } // === FETCH per il sync (implementazione reale usando RemoteJsonClient) === Future> _fetchAllRemoteItems() async { try { final rs = await _safeLoadRemoteSettings(); if (!rs.enabled || rs.baseUrl.trim().isEmpty) { debugPrint('[remote-sync][fetch] disabled or baseUrl empty'); return []; } // Costruisci l'auth solo se sono presenti credenziali 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 from ${rs.baseUrl}'); return items; } catch (e, st) { debugPrint('[remote-sync][fetch] client.fetchAll ERROR: $e\n$st'); return []; } } catch (e, st) { debugPrint('[remote-sync][fetch] ERROR: $e\n$st'); return []; } } // --- Helper di debug: crea una lens temporanea, conta i remoti, poi dispose int _countRemotesInSource(CollectionSource source) { final lens = CollectionLens(source: source, filters: {}); try { return lens.sortedEntries.where((e) => e.origin == 1 && e.trashed == 0).length; } finally { lens.dispose(); } } // === DIAG: stampa conteggi remoti DB/Source/visibleEntries === Future _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 _initViewerEssentials() async { // for video playback storage await localMediaDb.init(); } bool _isViewerSourceable(AvesEntry? viewerEntry) { return viewerEntry != null && viewerEntry.directory != null && !settings.hiddenFilters.any((filter) => filter.test(viewerEntry)); } Future _initViewerEntry({required String uri, required String? mimeType}) async { if (uri.startsWith('/')) { // convert this file path to a proper URI uri = Uri.file(uri).toString(); } final entry = await mediaFetchService.getEntry(uri, mimeType); if (entry != null) { // cataloguing is essential for coordinates and video rotation await entry.catalog(background: false, force: false, persist: false); } return entry; } // === DEBUG: apre la pagina di test remota con una seconda connessione al DB === Future _openRemoteTestPage(BuildContext context) async { if (_remoteTestOpen) return; // evita doppi push/sovrapposizioni // blocca solo se il sync è effettivamente in corso 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'); // Apri il DB in R/W (istanza indipendente) → niente "read only database" 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 _openRemoteSettingsDialog(BuildContext context) async { final s = await _safeLoadRemoteSettings(); final formKey = GlobalKey(); 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( 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(); // forza refresh immediato delle impostazioni e headers 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 _getRedirectRoute(AppMode appMode) async { String routeName; Set? 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(); final album = viewerEntry.directory; if (album != null) { // wait for collection to pass the `loading` state 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; // ⚠️ NON lanciamo più il sync qui (evita contese durante il viewer) // unawaited(rrs.runRemoteSyncOnceManaged()); collection = CollectionLens( source: source, filters: {StoredAlbumFilter(album, source.getStoredAlbumDisplayName(context, album))}, listenToSource: false, // if we group bursts, opening a burst sub-entry should: // - identify and select the containing main entry, // - select the sub-entry in the Viewer page. 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: (_) { return EntryViewerPage( collection: collection, initialEntry: viewerEntry, ); }, ); case AppMode.edit: return DirectMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), builder: (_) { return 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(); 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: // Wrapper di debug che aggiunge i due FAB (solo in debug) return buildRoute( (context) => _wrapWithRemoteDebug( context, CollectionPage(source: source, filters: filters), ), ); } } // ------------------------- // Utility sicure per remote // ------------------------- // safe load of RemoteSettings with timeout and fallback Future _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, ); } } // safe headers retrieval with timeout and empty fallback Future> _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 {}; } } // debug helper: clear remote keys from secure storage (debug only) Future _debugClearRemoteKeys() async { if (!kDebugMode) return; try { // FlutterSecureStorage non è const 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'); debugPrint('[remote] debugClearRemoteKeys executed'); } catch (e) { debugPrint('[remote] debugClearRemoteKeys failed: $e'); } } }