867 lines
32 KiB
Text
867 lines
32 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';
|
||
|
||
// --- 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');
|
||
|
||
// 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>();
|
||
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');
|
||
}
|
||
} 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();
|
||
}
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|
||
}
|