From ace841212e8772293c6faebf96b52a1eda556d95 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 24 Jun 2024 00:47:27 +0200 Subject: [PATCH] #592 #910 explorer --- CHANGELOG.md | 1 + lib/l10n/app_en.arb | 2 + lib/model/filters/album.dart | 4 +- lib/model/filters/aspect_ratio.dart | 2 +- lib/model/filters/coordinate.dart | 2 +- lib/model/filters/date.dart | 2 +- lib/model/filters/favourite.dart | 2 +- lib/model/filters/filters.dart | 2 +- lib/model/filters/location.dart | 2 +- lib/model/filters/mime.dart | 2 +- lib/model/filters/missing.dart | 2 +- lib/model/filters/or.dart | 4 +- lib/model/filters/path.dart | 17 ++ lib/model/filters/placeholder.dart | 2 +- lib/model/filters/query.dart | 2 +- lib/model/filters/rating.dart | 2 +- lib/model/filters/recent.dart | 2 +- lib/model/filters/tag.dart | 4 +- lib/model/filters/trash.dart | 2 +- lib/model/filters/type.dart | 2 +- lib/model/settings/enums/home_page.dart | 3 + lib/theme/icons.dart | 1 + lib/view/src/settings/enums.dart | 1 + lib/view/view.dart | 1 + .../quick_choosers/album_chooser.dart | 2 +- .../quick_choosers/tag_chooser.dart | 2 +- .../common/behaviour/pop/tv_navigation.dart | 4 +- lib/widgets/common/expandable_filter_row.dart | 2 +- .../common/identity/aves_filter_chip.dart | 40 +-- lib/widgets/dialogs/convert_entry_dialog.dart | 1 - .../entry_editors/rename_entry_set_page.dart | 11 +- lib/widgets/explorer/explorer_page.dart | 250 ++++++++++++++++++ .../common/covered_filter_chip.dart | 3 +- .../filter_grids/common/list_details.dart | 2 +- lib/widgets/home_page.dart | 7 +- .../navigation/drawer/page_nav_tile.dart | 7 +- lib/widgets/navigation/nav_display.dart | 21 +- lib/widgets/settings/navigation/drawer.dart | 2 + .../settings/navigation/navigation.dart | 1 + .../aves_model/lib/src/settings/enums.dart | 2 +- 40 files changed, 358 insertions(+), 65 deletions(-) create mode 100644 lib/widgets/explorer/explorer_page.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 47a617803..6146aedac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Collection: stack RAW and JPEG with same file names - Collection: ask to rename/replace/skip when converting items with name conflict - Export: bulk converting motion photos to still images +- Explorer: view folder tree and filter paths ### Fixed diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4ceb286db..9e405089e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -771,6 +771,8 @@ "binPageTitle": "Recycle Bin", + "explorerPageTitle": "Explorer", + "searchCollectionFieldHint": "Search collection", "searchRecentSectionTitle": "Recent", "searchDateSectionTitle": "Date", diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index d2126c938..53121a7ec 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -52,13 +52,13 @@ class AlbumFilter extends CoveredCollectionFilter { String getTooltip(BuildContext context) => album; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) { return IconUtils.getAlbumIcon( context: context, albumPath: album, size: size, ) ?? - (showGenericIcon ? Icon(AIcons.album, size: size) : null); + (allowGenericIcon ? Icon(AIcons.album, size: size) : null); } @override diff --git a/lib/model/filters/aspect_ratio.dart b/lib/model/filters/aspect_ratio.dart index b638ef7e4..5fcf6933f 100644 --- a/lib/model/filters/aspect_ratio.dart +++ b/lib/model/filters/aspect_ratio.dart @@ -68,7 +68,7 @@ class AspectRatioFilter extends CollectionFilter { } @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.aspectRatio, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.aspectRatio, size: size); @override String get category => type; diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index fff9df429..c8ba7f2a7 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -69,7 +69,7 @@ class CoordinateFilter extends CollectionFilter { } @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.geoBounds, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.geoBounds, size: size); @override String get category => type; diff --git a/lib/model/filters/date.dart b/lib/model/filters/date.dart index 2756de2e0..d73b8ab59 100644 --- a/lib/model/filters/date.dart +++ b/lib/model/filters/date.dart @@ -122,7 +122,7 @@ class DateFilter extends CollectionFilter { } @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.date, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.date, size: size); @override String get category => type; diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 2bd813b89..fc8855260 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -45,7 +45,7 @@ class FavouriteFilter extends CollectionFilter { String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.favourite, size: size); @override Future color(BuildContext context) { diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index f22270c88..fe52ebbdf 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -133,7 +133,7 @@ abstract class CollectionFilter extends Equatable implements Comparable getLabel(context); - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null; + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => null; Future color(BuildContext context) { final colors = context.read(); diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 0b5c06d25..7c29eddf0 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -89,7 +89,7 @@ class LocationFilter extends CoveredCollectionFilter { String getLabel(BuildContext context) => _isUnlocated ? context.l10n.filterNoLocationLabel : _location; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) { if (_isUnlocated) { return Icon(AIcons.locationUnlocated, size: size); } diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 86d4840d2..36176697d 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -77,7 +77,7 @@ class MimeFilter extends CollectionFilter { } @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size); @override Future color(BuildContext context) { diff --git a/lib/model/filters/missing.dart b/lib/model/filters/missing.dart index d785bc3c2..1f9bc6bf1 100644 --- a/lib/model/filters/missing.dart +++ b/lib/model/filters/missing.dart @@ -70,7 +70,7 @@ class MissingFilter extends CollectionFilter { } @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size); @override String get category => type; diff --git a/lib/model/filters/or.dart b/lib/model/filters/or.dart index 89e62751f..2b6c8d83f 100644 --- a/lib/model/filters/or.dart +++ b/lib/model/filters/or.dart @@ -60,8 +60,8 @@ class OrFilter extends CollectionFilter { String getLabel(BuildContext context) => _filters.map((v) => v.getLabel(context)).join(', '); @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { - return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, showGenericIcon: showGenericIcon); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) { + return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, allowGenericIcon: allowGenericIcon); } @override diff --git a/lib/model/filters/path.dart b/lib/model/filters/path.dart index f8b05afc7..4bb6c7cde 100644 --- a/lib/model/filters/path.dart +++ b/lib/model/filters/path.dart @@ -1,5 +1,9 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; +import 'package:flutter/widgets.dart'; class PathFilter extends CollectionFilter { static const type = 'path'; @@ -47,6 +51,19 @@ class PathFilter extends CollectionFilter { @override String get universalLabel => path; + @override + String getLabel(BuildContext context) { + final _directory = androidFileUtils.relativeDirectoryFromPath(path); + if (_directory == null) return universalLabel; + if (_directory.relativeDir.isEmpty) { + return _directory.getVolumeDescription(context); + } + return pContext.split(_directory.relativeDir).last; + } + + @override + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.explorer, size: size); + @override String get category => type; diff --git a/lib/model/filters/placeholder.dart b/lib/model/filters/placeholder.dart index 9e3263786..4f6145f7f 100644 --- a/lib/model/filters/placeholder.dart +++ b/lib/model/filters/placeholder.dart @@ -96,7 +96,7 @@ class PlaceholderFilter extends CollectionFilter { } @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size); @override String get category => type; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index af136e6ae..e5c47c42b 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -82,7 +82,7 @@ class QueryFilter extends CollectionFilter { String get universalLabel => query; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.text, size: size); @override Future color(BuildContext context) { diff --git a/lib/model/filters/rating.dart b/lib/model/filters/rating.dart index c9bd56290..ac296d365 100644 --- a/lib/model/filters/rating.dart +++ b/lib/model/filters/rating.dart @@ -64,7 +64,7 @@ class RatingFilter extends CollectionFilter { }; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) { return switch (rating) { -1 => Icon(AIcons.ratingRejected, size: size), 0 => Icon(AIcons.ratingUnrated, size: size), diff --git a/lib/model/filters/recent.dart b/lib/model/filters/recent.dart index 57d61e856..57d1b1e52 100644 --- a/lib/model/filters/recent.dart +++ b/lib/model/filters/recent.dart @@ -51,7 +51,7 @@ class RecentlyAddedFilter extends CollectionFilter { String getLabel(BuildContext context) => context.l10n.filterRecentlyAddedLabel; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.dateRecent, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.dateRecent, size: size); @override String get category => type; diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index cbb723e04..8282ad871 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -47,8 +47,8 @@ class TagFilter extends CoveredCollectionFilter { String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterNoTagLabel : tag; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { - return showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagUntagged : AIcons.tag, size: size) : null; + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) { + return allowGenericIcon ? Icon(tag.isEmpty ? AIcons.tagUntagged : AIcons.tag, size: size) : null; } @override diff --git a/lib/model/filters/trash.dart b/lib/model/filters/trash.dart index fc7b10325..095a058f9 100644 --- a/lib/model/filters/trash.dart +++ b/lib/model/filters/trash.dart @@ -41,7 +41,7 @@ class TrashFilter extends CollectionFilter { String getLabel(BuildContext context) => context.l10n.filterBinLabel; @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.bin, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.bin, size: size); @override String get category => type; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 1299b484e..89e77762d 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -99,7 +99,7 @@ class TypeFilter extends CollectionFilter { } @override - Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); + Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size); @override Future color(BuildContext context) { diff --git a/lib/model/settings/enums/home_page.dart b/lib/model/settings/enums/home_page.dart index cf33a0fc4..548e76f69 100644 --- a/lib/model/settings/enums/home_page.dart +++ b/lib/model/settings/enums/home_page.dart @@ -1,4 +1,5 @@ import 'package:aves/widgets/collection/collection_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_model/aves_model.dart'; @@ -12,6 +13,8 @@ extension ExtraHomePageSetting on HomePageSetting { return AlbumListPage.routeName; case HomePageSetting.tags: return TagListPage.routeName; + case HomePageSetting.explorer: + return ExplorerPage.routeName; } } } diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index f03f482cb..1aa37db63 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -29,6 +29,7 @@ class AIcons { static const disc = Icons.fiber_manual_record; static const display = Icons.light_mode_outlined; static const error = Icons.error_outline; + static const explorer = Icons.account_tree_outlined; static const folder = Icons.folder_outlined; static const geoBounds = Icons.public_outlined; static final github = MdiIcons.github; diff --git a/lib/view/src/settings/enums.dart b/lib/view/src/settings/enums.dart index 7e4277958..064c9619b 100644 --- a/lib/view/src/settings/enums.dart +++ b/lib/view/src/settings/enums.dart @@ -83,6 +83,7 @@ extension ExtraHomePageSettingView on HomePageSetting { HomePageSetting.collection => l10n.drawerCollectionAll, HomePageSetting.albums => l10n.drawerAlbumPage, HomePageSetting.tags => l10n.drawerTagPage, + HomePageSetting.explorer => l10n.explorerPageTitle, }; } } diff --git a/lib/view/view.dart b/lib/view/view.dart index fd3cfd0ff..097448a81 100644 --- a/lib/view/view.dart +++ b/lib/view/view.dart @@ -7,6 +7,7 @@ export 'src/actions/map_cluster.dart'; export 'src/actions/share.dart'; export 'src/actions/slideshow.dart'; export 'src/editor/enums.dart'; +export 'src/metadata/convert_action.dart'; export 'src/metadata/date_edit_action.dart'; export 'src/metadata/date_field_source.dart'; export 'src/metadata/fields.dart'; diff --git a/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart b/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart index 73e87d8dc..f1b5b7ee5 100644 --- a/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart +++ b/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart @@ -35,7 +35,7 @@ class AlbumQuickChooser extends StatelessWidget { pointerGlobalPosition: pointerGlobalPosition, itemBuilder: (context, album) => AvesFilterChip( filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), - showGenericIcon: false, + allowGenericIcon: false, ), ); } diff --git a/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart b/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart index fca80cea3..1947814f2 100644 --- a/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart +++ b/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart @@ -32,7 +32,7 @@ class TagQuickChooser extends StatelessWidget { pointerGlobalPosition: pointerGlobalPosition, itemBuilder: (context, filter) => AvesFilterChip( filter: filter, - showGenericIcon: false, + allowGenericIcon: false, ), ); } diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart index a8a5250ba..b55953eb8 100644 --- a/lib/widgets/common/behaviour/pop/tv_navigation.dart +++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart @@ -4,6 +4,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/extensions/build_context.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_model/aves_model.dart'; @@ -32,7 +33,7 @@ class TvNavigationPopHandler { return switch (homePage) { HomePageSetting.collection => context.read().filters.isEmpty, - HomePageSetting.albums || HomePageSetting.tags => true, + HomePageSetting.albums || HomePageSetting.tags || HomePageSetting.explorer => true, }; } @@ -47,6 +48,7 @@ class TvNavigationPopHandler { HomePageSetting.collection => buildRoute((context) => CollectionPage(source: context.read(), filters: null)), HomePageSetting.albums => buildRoute((context) => const AlbumListPage()), HomePageSetting.tags => buildRoute((context) => const TagListPage()), + HomePageSetting.explorer => buildRoute((context) => const ExplorerPage()), }; } } diff --git a/lib/widgets/common/expandable_filter_row.dart b/lib/widgets/common/expandable_filter_row.dart index f7e3e8e0c..f96cdf7e0 100644 --- a/lib/widgets/common/expandable_filter_row.dart +++ b/lib/widgets/common/expandable_filter_row.dart @@ -170,7 +170,7 @@ class ExpandableFilterRow extends StatelessWidget { // key `album-{path}` is expected by test driver key: Key(filter.key), filter: filter, - showGenericIcon: showGenericIcon, + allowGenericIcon: showGenericIcon, leadingOverride: leadingBuilder?.call(filter), heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap, onTap: onTap, diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 6cd848ca5..c5c7b313b 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -47,7 +47,7 @@ class AvesFilterDecoration { class AvesFilterChip extends StatefulWidget { final CollectionFilter filter; - final bool showText, showGenericIcon, useFilterColor; + final bool showLeading, showText, allowGenericIcon, useFilterColor; final AvesFilterDecoration? decoration; final Color? background; final String? banner; @@ -61,7 +61,7 @@ class AvesFilterChip extends StatefulWidget { static const double defaultRadius = 32; static const double outlineWidth = 2; static const double minChipHeight = kMinInteractiveDimension; - static const double minChipWidth = 80; + static const double minChipWidth = kMinInteractiveDimension; static const double iconSize = 18; static const double fontSize = 14; static const double decoratedContentVerticalPadding = 5; @@ -69,8 +69,9 @@ class AvesFilterChip extends StatefulWidget { const AvesFilterChip({ super.key, required this.filter, + this.showLeading = true, this.showText = true, - this.showGenericIcon = true, + this.allowGenericIcon = true, this.useFilterColor = true, this.decoration, this.background, @@ -255,10 +256,12 @@ class _AvesFilterChipState extends State { : null; Widget? content; - if (widget.showText) { + final showLeading = widget.showLeading; + final showText = widget.showText; + if (showLeading || showText) { final textScaler = MediaQuery.textScalerOf(context); final iconSize = textScaler.scale(AvesFilterChip.iconSize); - final leading = widget.leadingOverride ?? filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon); + final leading = showLeading ? widget.leadingOverride ?? filter.iconBuilder(context, iconSize, allowGenericIcon: widget.allowGenericIcon) : null; final trailing = onRemove != null ? Theme( data: Theme.of(context).copyWith( @@ -278,22 +281,21 @@ class _AvesFilterChipState extends State { mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (leading != null) ...[ - leading, - SizedBox(width: padding), - ], - Flexible( - child: Text( - filter.getLabel(context), - style: TextStyle( - fontSize: AvesFilterChip.fontSize, - decoration: filter.reversed ? TextDecoration.lineThrough : null, - decorationThickness: 2, + if (leading != null) leading, + if (leading != null && showText) SizedBox(width: padding), + if (showText) + Flexible( + child: Text( + filter.getLabel(context), + style: TextStyle( + fontSize: AvesFilterChip.fontSize, + decoration: filter.reversed ? TextDecoration.lineThrough : null, + decorationThickness: 2, + ), + softWrap: false, + overflow: TextOverflow.fade, ), - softWrap: false, - overflow: TextOverflow.fade, ), - ), if (trailing != null) ...[ SizedBox(width: padding), trailing, diff --git a/lib/widgets/dialogs/convert_entry_dialog.dart b/lib/widgets/dialogs/convert_entry_dialog.dart index 947fa113e..885116cd3 100644 --- a/lib/widgets/dialogs/convert_entry_dialog.dart +++ b/lib/widgets/dialogs/convert_entry_dialog.dart @@ -8,7 +8,6 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/text.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/utils/mime_utils.dart'; -import 'package:aves/view/src/metadata/convert_action.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/list_tiles/slider.dart'; import 'package:aves/widgets/common/basic/text/change_highlight.dart'; diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart index b46652c4c..b3962c5fc 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart @@ -6,7 +6,7 @@ import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/styles.dart'; -import 'package:aves/view/src/metadata/fields.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/popup/expansion_panel.dart'; @@ -122,11 +122,10 @@ class _RenameEntrySetPageState extends State { ...[ MetadataField.exifMake, MetadataField.exifModel, - ] - .map((field) => PopupMenuItem( - value: MetadataFieldNamingProcessor.keyWithField(field), - child: MenuRow(text: field.title), - )), + ].map((field) => PopupMenuItem( + value: MetadataFieldNamingProcessor.keyWithField(field), + child: MenuRow(text: field.title), + )), PopupMenuItem( value: HashNamingProcessor.key, child: MenuRow(text: l10n.renameProcessorHash), diff --git a/lib/widgets/explorer/explorer_page.dart b/lib/widgets/explorer/explorer_page.dart new file mode 100644 index 000000000..2695a3810 --- /dev/null +++ b/lib/widgets/explorer/explorer_page.dart @@ -0,0 +1,250 @@ +import 'dart:io'; + +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/path.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/themes.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; +import 'package:aves/widgets/common/basic/popup/menu_row.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/common/search/route.dart'; +import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; +import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; + +class ExplorerPage extends StatefulWidget { + static const routeName = '/explorer'; + + const ExplorerPage({super.key}); + + @override + State createState() => _ExplorerPageState(); +} + +class _ExplorerPageState extends State { + late VolumeRelativeDirectory _directory; + List? _contents; + + Set get volumes => androidFileUtils.storageVolumes; + + String get currentDirectoryPath => pContext.join(_directory.volumePath, _directory.relativeDir); + + @override + void initState() { + super.initState(); + final primaryVolume = volumes.firstWhereOrNull((v) => v.isPrimary); + if (primaryVolume != null) { + _goTo(primaryVolume.path); + } + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final visibleContents = _contents?.where((v) { + final isHidden = pContext.split(v.path).last.startsWith('.'); + return !isHidden; + }).toList(); + return PopScope( + canPop: _directory.relativeDir.isEmpty, + onPopInvoked: (didPop) { + if (didPop) return; + + final parent = pContext.dirname(currentDirectoryPath); + _goTo(parent); + setState(() {}); + }, + child: AvesScaffold( + appBar: _buildAppBar(context), + drawer: const AppDrawer(), + body: SafeArea( + child: Column( + children: [ + SizedBox( + height: kMinInteractiveDimension, + child: CrumbLine( + directory: _directory, + onTap: (path) { + _goTo(path); + setState(() {}); + }, + ), + ), + const Divider(height: 0), + Expanded( + child: visibleContents == null + ? const SizedBox() + : visibleContents.isEmpty + ? Center( + child: EmptyContent( + icon: AIcons.folder, + text: l10n.filePickerNoItems, + ), + ) + : ListView.builder( + itemCount: visibleContents.length, + itemBuilder: (context, index) { + return index < visibleContents.length ? _buildContentLine(context, visibleContents[index]) : const SizedBox(); + }, + ), + ), + const Divider(height: 0), + Padding( + padding: const EdgeInsets.all(8), + child: AvesFilterChip( + filter: PathFilter(currentDirectoryPath), + maxWidth: double.infinity, + onTap: (filter) => _goToCollectionPage(context, filter), + onLongPress: null, + ), + ), + ], + ), + ), + ), + ); + } + + AppBar _buildAppBar(BuildContext context) { + final animations = context.select((s) => s.accessibilityAnimations); + + return AppBar( + title: InteractiveAppBarTitle( + onTap: _goToSearch, + child: Text( + context.l10n.explorerPageTitle, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ), + actions: [ + if (volumes.length > 1) + FontSizeIconTheme( + child: PopupMenuButton( + itemBuilder: (context) { + return volumes.map((v) { + final selected = _directory.volumePath == v.path; + final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain; + return PopupMenuItem( + value: v, + enabled: !selected, + child: MenuRow( + text: v.getDescription(context), + icon: Icon(icon), + ), + ); + }).toList(); + }, + onSelected: (volume) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(animations.popUpAnimationDelay * timeDilation); + + Navigator.maybeOf(context)?.pop(); + await Future.delayed(ADurations.drawerTransitionAnimation); + _goTo(volume.path); + setState(() {}); + }, + popUpAnimationStyle: animations.popUpAnimationStyle, + ), + ), + ], + ); + } + + String? _getAlbumPath(CollectionSource source, FileSystemEntity content) { + final contentPath = content.path.toLowerCase(); + return source.rawAlbums.firstWhereOrNull((v) => v.toLowerCase() == contentPath); + } + + Widget _buildContentLine(BuildContext context, FileSystemEntity content) { + final source = context.read(); + final album = _getAlbumPath(source, content); + final baseIconTheme = IconTheme.of(context); + + return ListTile( + leading: const Icon(AIcons.folder), + title: Text('${Unicode.FSI}${pContext.split(content.path).last}${Unicode.PDI}'), + trailing: album != null + ? IconTheme.merge( + data: baseIconTheme, + child: AvesFilterChip( + filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), + showText: false, + maxWidth: AvesFilterChip.minChipWidth, + onTap: (filter) => _goToCollectionPage(context, filter), + onLongPress: null, + ), + ) + : null, + onTap: () { + _goTo(content.path); + setState(() {}); + }, + ); + } + + void _goTo(String path) { + _directory = androidFileUtils.relativeDirectoryFromPath(path)!; + _contents = null; + final contents = []; + + final source = context.read(); + final albums = source.rawAlbums.map((v) => v.toLowerCase()).toSet(); + Directory(currentDirectoryPath).list().listen((event) { + final entity = event.absolute; + if (entity is Directory) { + final dirPath = entity.path.toLowerCase(); + if (albums.any((v) => v.startsWith(dirPath))) { + contents.add(entity); + } + } + }, onDone: () { + _contents = contents..sort((a, b) => compareAsciiUpperCaseNatural(pContext.split(a.path).last, pContext.split(b.path).last)); + setState(() {}); + }); + } + + void _goToSearch() { + Navigator.maybeOf(context)?.push( + SearchPageRoute( + delegate: CollectionSearchDelegate( + searchFieldLabel: context.l10n.searchCollectionFieldHint, + searchFieldStyle: Themes.searchFieldStyle(context), + source: context.read(), + ), + ), + ); + } + + void _goToCollectionPage(BuildContext context, CollectionFilter filter) { + Navigator.maybeOf(context)?.push( + MaterialPageRoute( + settings: const RouteSettings(name: CollectionPage.routeName), + builder: (context) => CollectionPage( + source: context.read(), + filters: {filter}, + ), + ), + ); + } +} diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart index 4bfda58ad..3581bed66 100644 --- a/lib/widgets/filter_grids/common/covered_filter_chip.dart +++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart @@ -115,8 +115,9 @@ class CoveredFilterChip extends StatelessWidget { return AvesFilterChip( key: chipKey, filter: _filter, + showLeading: showText, showText: showText, - showGenericIcon: false, + allowGenericIcon: false, decoration: AvesFilterDecoration( radius: radius(extent), widget: Padding( diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index ad16a15f8..3f974cdcb 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.dart @@ -33,7 +33,7 @@ class FilterListDetails extends StatelessWidget { Widget build(BuildContext context) { final detailsTheme = context.watch(); - final leading = filter.iconBuilder(context, detailsTheme.titleIconSize, showGenericIcon: false); + final leading = filter.iconBuilder(context, detailsTheme.titleIconSize, allowGenericIcon: false); final hasTitleLeading = leading != null; return Container( diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index a96fe888b..42273d101 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -25,6 +25,7 @@ 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/intent.dart'; @@ -346,12 +347,14 @@ class _HomePageState extends State { return buildRoute((context) => const AlbumListPage()); case TagListPage.routeName: return buildRoute((context) => const TagListPage()); + case ExplorerPage.routeName: + return buildRoute((context) => const ExplorerPage()); + 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 HomeWidgetSettingsPage.routeName: - return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!)); case SearchPage.routeName: return SearchPageRoute( delegate: CollectionSearchDelegate( diff --git a/lib/widgets/navigation/drawer/page_nav_tile.dart b/lib/widgets/navigation/drawer/page_nav_tile.dart index fedcaffa9..a6ff3f63b 100644 --- a/lib/widgets/navigation/drawer/page_nav_tile.dart +++ b/lib/widgets/navigation/drawer/page_nav_tile.dart @@ -6,6 +6,7 @@ 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/debug/app_debug_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/countries_page.dart'; import 'package:aves/widgets/filter_grids/places_page.dart'; @@ -95,12 +96,14 @@ class PageNavTile extends StatelessWidget { return (_) => const PlaceListPage(); case TagListPage.routeName: return (_) => const TagListPage(); - case SettingsPage.routeName: - return (_) => const SettingsPage(); case AboutPage.routeName: return (_) => const AboutPage(); case AppDebugPage.routeName: return (_) => const AppDebugPage(); + case ExplorerPage.routeName: + return (_) => const ExplorerPage(); + case SettingsPage.routeName: + return (_) => const SettingsPage(); default: throw Exception('unknown route=$routeName'); } diff --git a/lib/widgets/navigation/nav_display.dart b/lib/widgets/navigation/nav_display.dart index 7a0d3fb14..343cad986 100644 --- a/lib/widgets/navigation/nav_display.dart +++ b/lib/widgets/navigation/nav_display.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/page.dart'; import 'package:aves/widgets/debug/app_debug_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/countries_page.dart'; import 'package:aves/widgets/filter_grids/places_page.dart'; @@ -40,14 +41,16 @@ class NavigationDisplay { return l10n.drawerPlacePage; case TagListPage.routeName: return l10n.drawerTagPage; - case SettingsPage.routeName: - return l10n.settingsPageTitle; case AboutPage.routeName: return l10n.aboutPageTitle; - case SearchPage.routeName: - return MaterialLocalizations.of(context).searchFieldLabel; case AppDebugPage.routeName: return 'Debug'; + case ExplorerPage.routeName: + return l10n.explorerPageTitle; + case SearchPage.routeName: + return MaterialLocalizations.of(context).searchFieldLabel; + case SettingsPage.routeName: + return l10n.settingsPageTitle; default: return route; } @@ -63,14 +66,16 @@ class NavigationDisplay { return AIcons.place; case TagListPage.routeName: return AIcons.tag; - case SettingsPage.routeName: - return AIcons.settings; case AboutPage.routeName: return AIcons.info; - case SearchPage.routeName: - return AIcons.search; case AppDebugPage.routeName: return AIcons.debug; + case ExplorerPage.routeName: + return AIcons.explorer; + case SearchPage.routeName: + return AIcons.search; + case SettingsPage.routeName: + return AIcons.settings; default: return null; } diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index 2b3c2c91c..ca7fd703f 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/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/countries_page.dart'; import 'package:aves/widgets/filter_grids/places_page.dart'; @@ -41,6 +42,7 @@ class _NavigationDrawerEditorPageState extends State CountryListPage.routeName, PlaceListPage.routeName, TagListPage.routeName, + ExplorerPage.routeName, SearchPage.routeName, }; diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 606079dd7..a23493848 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -73,6 +73,7 @@ class SettingsTileNavigationHomePage extends SettingsTile { const _HomeOption(HomePageSetting.collection), const _HomeOption(HomePageSetting.albums), const _HomeOption(HomePageSetting.tags), + const _HomeOption(HomePageSetting.explorer), if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection), ], getName: (context, v) => v.getName(context), diff --git a/plugins/aves_model/lib/src/settings/enums.dart b/plugins/aves_model/lib/src/settings/enums.dart index e8088d359..fce0ea57e 100644 --- a/plugins/aves_model/lib/src/settings/enums.dart +++ b/plugins/aves_model/lib/src/settings/enums.dart @@ -14,7 +14,7 @@ enum DisplayRefreshRateMode { auto, highest, lowest } enum EntryBackground { black, white, checkered } -enum HomePageSetting { collection, albums, tags } +enum HomePageSetting { collection, albums, tags, explorer } enum KeepScreenOn { never, videoPlayback, viewerOnly, always }