import 'dart:async'; import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/home_page.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/model/source/enums.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/viewer_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; class HomePage extends StatefulWidget { static const routeName = '/'; // untyped map as it is coming from the platform final Map? intentData; const HomePage({ Key? key, this.intentData, }) : super(key: key); @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State { AvesEntry? _viewerEntry; String? _shortcutRouteName, _shortcutSearchQuery; Set? _shortcutFilters; static const allowedShortcutRoutes = [CollectionPage.routeName, AlbumListPage.routeName, SearchPage.routeName]; @override void initState() { super.initState(); _setup(); imageCache!.maximumSizeBytes = 512 * (1 << 20); } @override Widget build(BuildContext context) => const Scaffold(); Future _setup() async { final stopwatch = Stopwatch()..start(); final permissions = await [ Permission.storage, // to access media with unredacted metadata with scoped storage (Android 10+) Permission.accessMediaLocation, ].request(); if (permissions[Permission.storage] != PermissionStatus.granted) { unawaited(SystemNavigator.pop()); return; } await androidFileUtils.init(); if (settings.isInstalledAppAccessAllowed) { // TODO TLAD transition code (it's unset in v1.5.4), remove in a later release settings.isInstalledAppAccessAllowed = settings.isInstalledAppAccessAllowed; unawaited(androidFileUtils.initAppNames()); } var appMode = AppMode.main; final intentData = widget.intentData ?? await ViewerService.getIntentData(); if (intentData.isNotEmpty) { final action = intentData['action']; await reportService.log('Intent data=$intentData'); switch (action) { case 'view': _viewerEntry = await _initViewerEntry( uri: intentData['uri'], mimeType: intentData['mimeType'], ); if (_viewerEntry != null) { appMode = AppMode.view; } break; case 'pick': appMode = AppMode.pickExternal; // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) String? pickMimeTypes = intentData['mimeType']; debugPrint('pick mimeType=$pickMimeTypes'); break; case 'search': _shortcutRouteName = SearchPage.routeName; _shortcutSearchQuery = intentData['query']; break; default: // do not use 'route' as extra key, as the Flutter framework acts on it final extraRoute = intentData['page']; if (allowedShortcutRoutes.contains(extraRoute)) { _shortcutRouteName = extraRoute; } final extraFilters = intentData['filters']; _shortcutFilters = extraFilters != null ? (extraFilters as List).cast().toSet() : null; } } context.read>().value = appMode; unawaited(reportService.setCustomKey('app_mode', appMode.toString())); if (appMode != AppMode.view || _isViewerSourceable(_viewerEntry!)) { debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms'); unawaited(GlobalSearch.registerCallback()); unawaited(AnalysisService.registerCallback()); final source = context.read(); await source.init(); unawaited(source.refresh()); } // `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.pushAndRemoveUntil( context, await _getRedirectRoute(appMode), (route) => false, )); } bool _isViewerSourceable(AvesEntry viewerEntry) => viewerEntry.directory != null && !settings.hiddenFilters.any((filter) => filter.test(viewerEntry)); Future _initViewerEntry({required String uri, required String? mimeType}) async { if (uri.startsWith('/')) { // convert this file path to a proper URI uri = Uri.file(uri).toString(); } final entry = await mediaFileService.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; } Future _getRedirectRoute(AppMode appMode) async { if (appMode == AppMode.view) { AvesEntry viewerEntry = _viewerEntry!; CollectionLens? collection; final source = context.read(); if (source.initialized) { final album = viewerEntry.directory; if (album != null) { // wait for collection to pass the `loading` state final completer = Completer(); void _onSourceStateChanged() { if (source.stateNotifier.value != SourceState.loading) { source.stateNotifier.removeListener(_onSourceStateChanged); completer.complete(); } } source.stateNotifier.addListener(_onSourceStateChanged); await completer.future; collection = CollectionLens( source: source, filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))}, ); 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, ); }, ); } String routeName; Set? filters; if (appMode == AppMode.pickExternal) { routeName = CollectionPage.routeName; } else { routeName = _shortcutRouteName ?? settings.homePage.routeName; filters = (_shortcutFilters ?? {}).map(CollectionFilter.fromJson).toSet(); } final source = context.read(); switch (routeName) { case AlbumListPage.routeName: return DirectMaterialPageRoute( settings: const RouteSettings(name: AlbumListPage.routeName), builder: (_) => const AlbumListPage(), ); case SearchPage.routeName: return SearchPageRoute( delegate: CollectionSearchDelegate( source: source, canPop: false, initialQuery: _shortcutSearchQuery, ), ); case CollectionPage.routeName: default: return DirectMaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (_) => CollectionPage( collection: CollectionLens( source: source, filters: filters, ), ), ); } } }