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

775 lines
27 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';
// --- 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');
}
// ============================================================
// ✅ NEW: wait for locals to be READY before starting remote bootstrap
// ============================================================
Future<void> _waitSourceReady(CollectionSource source) async {
if (source.stateNotifier.value == SourceState.ready) return;
final c = Completer<void>();
void onState() {
if (source.stateNotifier.value == SourceState.ready) {
source.stateNotifier.removeListener(onState);
c.complete();
}
}
source.stateNotifier.addListener(onState);
onState();
await c.future;
}
// ============================================================
// 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>();
// 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
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
try {
final curCount = source.visibleEntries.isNotEmpty ? source.visibleEntries.length : source.allEntries.length;
if (curCount < 50) {
final locals = await localMediaDb.loadEntries(origin: 0);
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');
}
Future.delayed(const Duration(seconds: 3), detach);
// Remoti:
if (await _isRemoteBootstrapDone()) {
debugPrint('[REMOTE] append from DB (bootstrap done)');
await source.appendRemoteEntriesFromDb();
} else {
debugPrint('[REMOTE] skip append from DB (bootstrap not done)');
}
// ✅ scheduling sync remoto:
// - se bootstrap -> aspetta che i LOCALI siano READY, poi avvia bootstrap remoto
// - se non bootstrap -> chiama pure (torna subito) oppure lascia comè
if (!_remoteSyncScheduled) {
_remoteSyncScheduled = true;
final sourceRef = source;
if (bootstrap) {
unawaited(() async {
debugPrint('[remote-sync] bootstrap requested -> wait local READY first');
await _waitSourceReady(sourceRef);
debugPrint('[remote-sync] local READY -> start bootstrap remote');
await _runRemoteSync(sourceRef, bootstrap: true);
}());
} else {
unawaited(Future.microtask(() => _runRemoteSync(sourceRef, bootstrap: false)));
}
}
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)
// ============================================================
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: 'Agg remoti…',
total: total,
showOverlay: bootstrap, // per punto 2, al bootstrap mostreremo anche contatore
);
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 remoti…', 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(phase: 'Remoti aggiornati');
} catch (e, st) {
debugPrint('[remote-sync] error: $e\n$st');
RemoteSyncBus.instance.fail(e);
} 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 {};
}
}
}