diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5f596ade3..a83c2ec03 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -323,8 +323,10 @@ - + diff --git a/lib/widgets/explorer/app_bar.dart b/lib/widgets/explorer/app_bar.dart index 2f5b86236..cdc0666c2 100644 --- a/lib/widgets/explorer/app_bar.dart +++ b/lib/widgets/explorer/app_bar.dart @@ -26,7 +26,7 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; class ExplorerAppBar extends StatefulWidget { - final ValueNotifier directoryNotifier; + final ValueNotifier directoryNotifier; final void Function(String path) goTo; const ExplorerAppBar({ @@ -67,7 +67,7 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse return SizedBox( width: constraints.maxWidth, height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)), - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: widget.directoryNotifier, builder: (context, directory, child) { return CrumbLine( @@ -128,7 +128,9 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse // wait for the popup menu to hide before proceeding with the action await Future.delayed(animations.popUpAnimationDelay * timeDilation); final directory = widget.directoryNotifier.value; - ExplorerActionDelegate(directory: directory).onActionSelected(context, action); + if (directory != null) { + ExplorerActionDelegate(directory: directory).onActionSelected(context, action); + } }, popUpAnimationStyle: animations.popUpAnimationStyle, ), @@ -137,10 +139,10 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse Widget _buildVolumeSelector(BuildContext context) { if (_volumes.length == 2) { - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: widget.directoryNotifier, builder: (context, directory, child) { - final currentVolume = directory.volumePath; + final currentVolume = directory?.volumePath; final otherVolume = _volumes.firstWhere((volume) => volume.path != currentVolume); final icon = otherVolume.isRemovable ? AIcons.storageCard : AIcons.storageMain; return IconButton( @@ -155,7 +157,7 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse icon: const Icon(AIcons.storageCard), onPressed: () async { _volumes.map((v) { - final selected = widget.directoryNotifier.value.volumePath == v.path; + final selected = widget.directoryNotifier.value?.volumePath == v.path; final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain; return PopupMenuItem( value: v, @@ -166,7 +168,7 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse ), ); }).toList(); - final volumePath = widget.directoryNotifier.value.volumePath; + final volumePath = widget.directoryNotifier.value?.volumePath; final initialVolume = _volumes.firstWhereOrNull((v) => v.path == volumePath); final volume = await showDialog( context: context, diff --git a/lib/widgets/explorer/explorer_page.dart b/lib/widgets/explorer/explorer_page.dart index bd7c801fc..859a70f72 100644 --- a/lib/widgets/explorer/explorer_page.dart +++ b/lib/widgets/explorer/explorer_page.dart @@ -41,15 +41,13 @@ class ExplorerPage extends StatefulWidget { class _ExplorerPageState extends State { final List _subscriptions = []; - final ValueNotifier _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: '')); + final ValueNotifier _directory = ValueNotifier(null); + final ValueNotifier _contentsDirectory = ValueNotifier(null); final ValueNotifier> _contents = ValueNotifier([]); Set get _volumes => androidFileUtils.storageVolumes; - String get _currentDirectoryPath { - final dir = _directory.value; - return pContext.join(dir.volumePath, dir.relativeDir); - } + String? _pathOf(VolumeRelativeDirectory? dir) => dir != null ? pContext.join(dir.volumePath, dir.relativeDir) : null; @override void initState() { @@ -82,15 +80,20 @@ class _ExplorerPageState extends State { @override Widget build(BuildContext context) { - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: _directory, builder: (context, directory, child) { - final atRoot = directory.relativeDir.isEmpty; + final atRoot = directory?.relativeDir.isEmpty ?? true; return AvesPopScope( handlers: [ APopHandler( canPop: (context) => atRoot, - onPopBlocked: (context) => _goTo(pContext.dirname(_currentDirectoryPath)), + onPopBlocked: (context) { + final path = _pathOf(directory); + if (path != null) { + _goTo(pContext.dirname(path)); + } + }, ), tvNavigationPopHandler, doubleBackPopHandler, @@ -118,7 +121,7 @@ class _ExplorerPageState extends State { AnimationLimiter( // animation limiter should not be above the app bar // so that the crumb line can automatically scroll - key: ValueKey(_currentDirectoryPath), + key: ValueKey(contents), child: SliverList.builder( itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( @@ -147,18 +150,26 @@ class _ExplorerPageState extends State { ), ), const Divider(height: 0), - SafeArea( - top: false, - bottom: true, - child: Padding( - padding: const EdgeInsets.all(8), - child: AvesFilterChip( - filter: PathFilter(_currentDirectoryPath), - maxWidth: double.infinity, - onTap: (filter) => _goToCollectionPage(context, filter), - onLongPress: null, - ), - ), + ValueListenableBuilder( + valueListenable: _contentsDirectory, + builder: (context, contentsDirectory, child) { + final dirPath = _pathOf(contentsDirectory); + return dirPath != null + ? SafeArea( + top: false, + bottom: true, + child: Padding( + padding: const EdgeInsets.all(8), + child: AvesFilterChip( + filter: PathFilter(dirPath), + maxWidth: double.infinity, + onTap: (filter) => _goToCollectionPage(context, filter), + onLongPress: null, + ), + ), + ) + : const SizedBox(); + }, ), ], ), @@ -177,15 +188,18 @@ class _ExplorerPageState extends State { if (loading) { bottom = const CircularProgressIndicator(); } else { - final source = context.read(); - final album = _getAlbumPath(source, Directory(_currentDirectoryPath)); - if (album != null) { - bottom = AvesFilterChip( - filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), - maxWidth: double.infinity, - onTap: (filter) => _goToCollectionPage(context, filter), - onLongPress: null, - ); + final dirPath = _pathOf(_contentsDirectory.value); + if (dirPath != null) { + final source = context.read(); + final album = _getAlbumPath(source, Directory(dirPath)); + if (album != null) { + bottom = AvesFilterChip( + filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), + maxWidth: double.infinity, + onTap: (filter) => _goToCollectionPage(context, filter), + onLongPress: null, + ); + } } } @@ -249,11 +263,14 @@ class _ExplorerPageState extends State { } void _updateContents() { - final contents = []; + final directory = _directory.value; + final dirPath = _pathOf(directory); + if (dirPath == null) return; + final contents = []; final source = context.read(); final albums = source.rawAlbums.map((v) => v.toLowerCase()).toSet(); - Directory(_currentDirectoryPath).list().listen((event) { + Directory(dirPath).list().listen((event) { final entity = event.absolute; if (entity is Directory) { final dirPath = entity.path.toLowerCase(); @@ -268,6 +285,7 @@ class _ExplorerPageState extends State { final nameB = pContext.split(b.path).last; return compareAsciiUpperCaseNatural(nameA, nameB); }); + _contentsDirectory.value = directory; }); } diff --git a/lib/widgets/settings/privacy/file_picker/crumb_line.dart b/lib/widgets/settings/privacy/file_picker/crumb_line.dart index d334807cc..6ba442830 100644 --- a/lib/widgets/settings/privacy/file_picker/crumb_line.dart +++ b/lib/widgets/settings/privacy/file_picker/crumb_line.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class CrumbLine extends StatefulWidget { - final VolumeRelativeDirectory directory; + final VolumeRelativeDirectory? directory; final void Function(String path) onTap; const CrumbLine({ @@ -25,7 +25,7 @@ class CrumbLine extends StatefulWidget { class _CrumbLineState extends State { final ScrollController _scrollController = ScrollController(); - VolumeRelativeDirectory get directory => widget.directory; + VolumeRelativeDirectory? get directory => widget.directory; @override void dispose() { @@ -36,7 +36,7 @@ class _CrumbLineState extends State { @override void didUpdateWidget(covariant CrumbLine oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.directory.relativeDir.length < widget.directory.relativeDir.length) { + if ((oldWidget.directory?.relativeDir.length ?? 0) < (widget.directory?.relativeDir.length ?? 0)) { // scroll to show last crumb WidgetsBinding.instance.addPostFrameCallback((_) { final animate = context.read().animate; @@ -56,10 +56,15 @@ class _CrumbLineState extends State { @override Widget build(BuildContext context) { - List parts = [ - directory.getVolumeDescription(context), - ...pContext.split(directory.relativeDir), - ]; + final _directory = directory; + final parts = []; + if (_directory != null) { + parts.addAll([ + _directory.getVolumeDescription(context), + ...pContext.split(_directory.relativeDir), + ]); + } + final crumbColor = DefaultTextStyle.of(context).style.color; return ListView.builder( scrollDirection: Axis.horizontal, @@ -84,13 +89,15 @@ class _CrumbLineState extends State { ); } return GestureDetector( - onTap: () { - final path = pContext.joinAll([ - directory.volumePath, - ...parts.skip(1).take(index), - ]); - widget.onTap(path); - }, + onTap: _directory != null + ? () { + final path = pContext.joinAll([ + _directory.volumePath, + ...parts.skip(1).take(index), + ]); + widget.onTap(path); + } + : null, child: Container( // use a `Container` with a dummy color to make it expand // so that we can also detect taps around the title `Text`