aves_mio1/lib/widgets/home/home_page.dart
FabioMich66 084fa184da
Some checks failed
Quality check / Flutter analysis (push) Has been cancelled
Quality check / CodeQL analysis (java-kotlin) (push) Has been cancelled
ok con video e foto in galleria aves
2026-03-17 12:19:38 +01:00

905 lines
34 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<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 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<void> _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<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:
// 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<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>();
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 = <CollectionFilter>{}; // All items (nessun filtro)
debugPrint('[startup] forcing CollectionPage All-items (remoti=$remCount)');
}
} catch (e) {
debugPrint('[startup] unable to count remotes: $e');
}
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// PERF: riattiva lanalisi 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>();
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<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>();
// 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<List<RemotePhotoItem>> _fetchAllRemoteItems() async {
try {
final rs = await _safeLoadRemoteSettings();
if (!rs.enabled || rs.baseUrl.trim().isEmpty) {
debugPrint('[remote-sync][fetch] disabled or baseUrl empty');
return <RemotePhotoItem>[];
}
// 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 <RemotePhotoItem>[];
}
} catch (e, st) {
debugPrint('[remote-sync][fetch] ERROR: $e\n$st');
return <RemotePhotoItem>[];
}
}
// --- 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<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 {
// 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<AvesEntry?> _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<void> _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<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();
// 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<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) {
// 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<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:
// 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<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,
);
}
}
// safe headers retrieval with timeout and empty fallback
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 {};
}
}
// debug helper: clear remote keys from secure storage (debug only)
Future<void> _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');
}
}
}