753 lines
27 KiB
Text
753 lines
27 KiB
Text
// 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 = '/';
|
|
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;
|
|
|
|
bool _remoteSyncScheduled = false;
|
|
bool _remoteSyncActive = false;
|
|
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');
|
|
}
|
|
|
|
// ============================================================
|
|
// INIT DEBUG (optional): SourceState + polling entry counts (3s)
|
|
// ============================================================
|
|
VoidCallback _attachInitDebug(CollectionSource source, String label) {
|
|
final sw = Stopwatch()..start();
|
|
int lastAll = -1;
|
|
int lastVis = -1;
|
|
|
|
void logState() {
|
|
debugPrint(
|
|
'[$label] state=${source.stateNotifier.value} '
|
|
't=${sw.elapsedMilliseconds}ms '
|
|
'all=${source.allEntries.length} vis=${source.visibleEntries.length} '
|
|
'loadedScope=${source.loadedScope}',
|
|
);
|
|
}
|
|
|
|
void pollCounts() {
|
|
final all = source.allEntries.length;
|
|
final vis = source.visibleEntries.length;
|
|
if (all != lastAll || vis != lastVis) {
|
|
lastAll = all;
|
|
lastVis = vis;
|
|
debugPrint('[$label] CHANGE t=${sw.elapsedMilliseconds}ms all=$all vis=$vis state=${source.stateNotifier.value}');
|
|
}
|
|
}
|
|
|
|
debugPrint('[$label] attach listeners');
|
|
logState();
|
|
pollCounts();
|
|
|
|
source.stateNotifier.addListener(logState);
|
|
final timer = Timer.periodic(const Duration(milliseconds: 100), (_) => pollCounts());
|
|
|
|
return () {
|
|
timer.cancel();
|
|
try {
|
|
source.stateNotifier.removeListener(logState);
|
|
} catch (_) {}
|
|
debugPrint('[$label] detach listeners at t=${sw.elapsedMilliseconds}ms');
|
|
};
|
|
}
|
|
|
|
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>();
|
|
|
|
// =========================================================
|
|
// MAIN INIT + LOCAL-HYDRATE + REMOTE APPEND/SYNC
|
|
// =========================================================
|
|
|
|
// cache DB?
|
|
bool hasAnyCache = false;
|
|
try {
|
|
await localMediaDb.init();
|
|
final rows = await localMediaDb.rawDb.rawQuery(
|
|
'SELECT 1 FROM entry WHERE trashed=0 LIMIT 1',
|
|
);
|
|
hasAnyCache = rows.isNotEmpty;
|
|
} catch (_) {}
|
|
|
|
final bootstrapDone = await _isRemoteBootstrapDone();
|
|
final bootstrap = !bootstrapDone;
|
|
|
|
debugPrint('[BOOT] hasAnyCache=$hasAnyCache bootstrapDone=$bootstrapDone bootstrap=$bootstrap '
|
|
'loadedScope=${source.loadedScope} state=${source.stateNotifier.value}');
|
|
|
|
final loadTopEntriesFirst =
|
|
settings.homeNavItem.route == CollectionPage.routeName &&
|
|
settings.homeCustomCollection.isEmpty;
|
|
|
|
final detach = _attachInitDebug(source, 'INIT');
|
|
|
|
// INIT (serve per inizializzare strutture interne)
|
|
final swInit = Stopwatch()..start();
|
|
debugPrint('[INIT] calling source.init(...) loadTopEntriesFirst=$loadTopEntriesFirst');
|
|
await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst);
|
|
swInit.stop();
|
|
debugPrint('[INIT] source.init DONE in ${swInit.elapsedMilliseconds}ms all=${source.allEntries.length} vis=${source.visibleEntries.length}');
|
|
|
|
// ---------------------------------------------------------
|
|
// ✅ LOCAL-HYDRATE (key feature): mostra subito i locali dal DB
|
|
// ---------------------------------------------------------
|
|
// Se dopo init la Source è ancora vuota/quasi vuota, iniettiamo cache DB origin=0.
|
|
// (Il tuo DB ha origin=0 -> 6957, quindi qui la galleria diventa istantanea.)
|
|
try {
|
|
final curCount = source.visibleEntries.isNotEmpty ? source.visibleEntries.length : source.allEntries.length;
|
|
if (curCount < 50) {
|
|
final locals = await localMediaDb.loadEntries(origin: 0); // Set<AvesEntry> nella tua codebase
|
|
debugPrint('[LOCAL-HYDRATE] db locals=${locals.length} curCount=$curCount');
|
|
|
|
if (locals.isNotEmpty) {
|
|
final existingUris = source.allEntries
|
|
.where((e) => e.origin == 0 && !e.trashed)
|
|
.map((e) => e.uri)
|
|
.whereType<String>()
|
|
.toSet();
|
|
|
|
final toAdd = locals.where((e) {
|
|
if (e.trashed) return false;
|
|
if (!e.isDisplayable) return false;
|
|
final u = e.uri;
|
|
if (u == null || u.isEmpty) return true;
|
|
return !existingUris.contains(u);
|
|
}).toSet();
|
|
|
|
if (toAdd.isNotEmpty) {
|
|
source.addEntries(toAdd);
|
|
debugPrint('[LOCAL-HYDRATE] added=${toAdd.length}');
|
|
} else {
|
|
debugPrint('[LOCAL-HYDRATE] nothing to add (duplicates/filtered)');
|
|
}
|
|
}
|
|
}
|
|
} catch (e, st) {
|
|
debugPrint('[LOCAL-HYDRATE] error: $e\n$st');
|
|
}
|
|
|
|
// stop debug logs after 3s
|
|
Future.delayed(const Duration(seconds: 3), detach);
|
|
|
|
// Remoti:
|
|
// - bootstrap done -> mostra subito dal DB
|
|
// - bootstrap not done -> compariranno alla fine del bootstrap sync
|
|
if (await _isRemoteBootstrapDone()) {
|
|
debugPrint('[REMOTE] append from DB (bootstrap done)');
|
|
await source.appendRemoteEntriesFromDb();
|
|
} else {
|
|
debugPrint('[REMOTE] skip append from DB (bootstrap not done)');
|
|
}
|
|
|
|
// schedule remote sync once
|
|
if (!_remoteSyncScheduled) {
|
|
_remoteSyncScheduled = true;
|
|
final sourceRef = source;
|
|
unawaited(Future.microtask(() => _runRemoteSync(sourceRef, bootstrap: bootstrap)));
|
|
}
|
|
|
|
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');
|
|
|
|
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)
|
|
// - full sync SOLO bootstrap
|
|
// - progress bar SOLO bootstrap
|
|
// - remoti visibili SOLO dopo bootstrap completato
|
|
// ============================================================
|
|
Future<void> _runRemoteSync(CollectionSource source, {required bool bootstrap}) async {
|
|
try {
|
|
final rs = await _safeLoadRemoteSettings();
|
|
if (!rs.enabled) {
|
|
debugPrint('[remote-sync] disabled → skip');
|
|
return;
|
|
}
|
|
|
|
if (!bootstrap) {
|
|
debugPrint('[remote-sync] bootstrap already done -> skip full sync (until Step 3 delta/ws)');
|
|
return;
|
|
}
|
|
|
|
_remoteSyncActive = true;
|
|
|
|
final items = await _fetchAllRemoteItems();
|
|
final total = items.length;
|
|
final serverIds = items.map((e) => e.id).where((v) => v.isNotEmpty).toSet();
|
|
|
|
RemoteSyncBus.instance.start(phase: 'Sync remoto…', total: total);
|
|
|
|
final repo = RemoteRepository(localMediaDb.rawDb);
|
|
await repo.deleteAllRemotes();
|
|
|
|
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);
|
|
}
|
|
|
|
final pruned = await repo.pruneMissingRemotes(serverIds);
|
|
debugPrint('[remote-sync] prune deleted=$pruned');
|
|
|
|
// remoti compaiono ora (bootstrap completato)
|
|
await source.appendRemoteEntriesFromDb();
|
|
await _setRemoteBootstrapDone();
|
|
|
|
RemoteSyncBus.instance.finish();
|
|
} catch (e, st) {
|
|
debugPrint('[remote-sync] error: $e\n$st');
|
|
RemoteSyncBus.instance.clear();
|
|
} finally {
|
|
_remoteSyncActive = false;
|
|
}
|
|
}
|
|
|
|
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>[];
|
|
}
|
|
}
|
|
|
|
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');
|
|
} finally {
|
|
try {
|
|
await debugDb?.close();
|
|
} catch (_) {}
|
|
_remoteTestOpen = false;
|
|
}
|
|
}
|
|
|
|
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: (_) => 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 {
|
|
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!),
|
|
);
|
|
|
|
default:
|
|
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) => CollectionPage(source: source, filters: filters));
|
|
}
|
|
}
|
|
|
|
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 {};
|
|
}
|
|
}
|
|
}
|