diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7ea56840c..1cd437d31 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -24,12 +24,20 @@ "@hideButtonLabel": {}, "continueButtonLabel": "CONTINUE", "@continueButtonLabel": {}, + "changeTooltip": "Change", + "@changeTooltip": {}, "clearTooltip": "Clear", "@clearTooltip": {}, "previousTooltip": "Previous", "@previousTooltip": {}, "nextTooltip": "Next", "@nextTooltip": {}, + "showTooltip": "Show", + "@showTooltip": {}, + "hideTooltip": "Hide", + "@hideTooltip": {}, + "removeTooltip": "Remove", + "@removeTooltip": {}, "doubleBackExitMessage": "Tap “back” again to exit.", "@doubleBackExitMessage": {}, @@ -476,10 +484,18 @@ "drawerCollectionAll": "All collection", "@drawerCollectionAll": {}, - "drawerCollectionVideos": "Videos", - "@drawerCollectionVideos": {}, "drawerCollectionFavourites": "Favourites", "@drawerCollectionFavourites": {}, + "drawerCollectionImages": "Images", + "@drawerCollectionImages": {}, + "drawerCollectionVideos": "Videos", + "@drawerCollectionVideos": {}, + "drawerCollectionMotionPhotos": "Motion photos", + "@drawerCollectionMotionPhotos": {}, + "drawerCollectionPanoramas": "Panoramas", + "@drawerCollectionPanoramas": {}, + "drawerCollectionSphericalVideos": "360° Videos", + "@drawerCollectionSphericalVideos": {}, "chipSortTitle": "Sort", "@chipSortTitle": {}, @@ -505,6 +521,8 @@ "@albumPickPageTitleExport": {}, "albumPickPageTitleMove": "Move to Album", "@albumPickPageTitleMove": {}, + "albumPickPageTitlePick": "Pick Album", + "@albumPickPageTitlePick": {}, "albumCamera": "Camera", "@albumCamera": {}, @@ -572,6 +590,21 @@ "settingsDoubleBackExit": "Tap “back” twice to exit", "@settingsDoubleBackExit": {}, + "settingsNavigationDrawerTile": "Navigation menu", + "@settingsNavigationDrawerTile": {}, + "settingsNavigationDrawerEditorTitle": "Navigation Menu", + "@settingsNavigationDrawerEditorTitle": {}, + "settingsNavigationDrawerBanner": "Touch and hold to move and reorder menu items.", + "@settingsNavigationDrawerBanner": {}, + "settingsNavigationDrawerTabTypes": "Types", + "@settingsNavigationDrawerTabTypes": {}, + "settingsNavigationDrawerTabAlbums": "Albums", + "@settingsNavigationDrawerTabAlbums": {}, + "settingsNavigationDrawerTabPages": "Pages", + "@settingsNavigationDrawerTabPages": {}, + "settingsNavigationDrawerAddAlbum": "Add album", + "@settingsNavigationDrawerAddAlbum": {}, + "settingsSectionThumbnails": "Thumbnails", "@settingsSectionThumbnails": {}, "settingsThumbnailShowLocationIcon": "Show location icon", @@ -683,8 +716,6 @@ "@settingsHiddenPathsBanner": {}, "settingsHiddenPathsEmpty": "No hidden paths", "@settingsHiddenPathsEmpty": {}, - "settingsHiddenPathsRemoveTooltip": "Remove", - "@settingsHiddenPathsRemoveTooltip": {}, "addPathTooltip": "Add path", "@addPathTooltip": {}, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 402f6a550..0002cb28a 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -10,9 +10,13 @@ "showButtonLabel": "보기", "hideButtonLabel": "숨기기", "continueButtonLabel": "다음", + "changeTooltip": "변경", "clearTooltip": "초기화", "previousTooltip": "이전", "nextTooltip": "다음", + "showTooltip": "보기", + "hideTooltip": "숨기기", + "removeTooltip": "제거", "doubleBackExitMessage": "종료하려면 한번 더 누르세요.", @@ -212,8 +216,12 @@ "collectionDeselectSectionTooltip": "묶음 선택 해제", "drawerCollectionAll": "모든 미디어", - "drawerCollectionVideos": "동영상", "drawerCollectionFavourites": "즐겨찾기", + "drawerCollectionImages": "사진", + "drawerCollectionVideos": "동영상", + "drawerCollectionMotionPhotos": "모션 포토", + "drawerCollectionPanoramas": "파노라마", + "drawerCollectionSphericalVideos": "360° 동영상", "chipSortTitle": "정렬", "chipSortDate": "날짜", @@ -228,6 +236,7 @@ "albumPickPageTitleCopy": "앨범으로 복사", "albumPickPageTitleExport": "앨범으로 내보내기", "albumPickPageTitleMove": "앨범으로 이동", + "albumPickPageTitlePick": "앨범 선택", "albumCamera": "카메라", "albumDownload": "다운로드", @@ -266,6 +275,14 @@ "settingsKeepScreenOnTitle": "화면 자동 꺼짐 방지", "settingsDoubleBackExit": "뒤로가기 두번 눌러 앱 종료하기", + "settingsNavigationDrawerTile": "탐색 메뉴", + "settingsNavigationDrawerEditorTitle": "탐색 메뉴", + "settingsNavigationDrawerBanner": "항목을 길게 누른 후 이동하여 탐색 메뉴에 표시될 항목의 순서를 수정하세요.", + "settingsNavigationDrawerTabTypes": "유형", + "settingsNavigationDrawerTabAlbums": "앨범", + "settingsNavigationDrawerTabPages": "페이지", + "settingsNavigationDrawerAddAlbum": "앨범 추가", + "settingsSectionThumbnails": "섬네일", "settingsThumbnailShowLocationIcon": "위치 아이콘 표시", "settingsThumbnailShowRawIcon": "Raw 아이콘 표시", @@ -325,7 +342,6 @@ "settingsHiddenPathsTitle": "숨겨진 경로", "settingsHiddenPathsBanner": "이 경로에 있는 사진과 동영상이 숨겨지고 있으며 이 앱에서 보여지지 않을 것입니다.", "settingsHiddenPathsEmpty": "숨겨진 경로가 없습니다", - "settingsHiddenPathsRemoveTooltip": "제거", "addPathTooltip": "경로 추가", "settingsStorageAccessTile": "저장공간 접근", diff --git a/lib/model/actions/chip_set_actions.dart b/lib/model/actions/chip_set_actions.dart index 8ce4fe066..b0dedc1c8 100644 --- a/lib/model/actions/chip_set_actions.dart +++ b/lib/model/actions/chip_set_actions.dart @@ -78,7 +78,7 @@ extension ExtraChipSetAction on ChipSetAction { case ChipSetAction.stats: return AIcons.stats; case ChipSetAction.createAlbum: - return AIcons.createAlbum; + return AIcons.add; // single/multiple filters case ChipSetAction.delete: return AIcons.delete; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 229d292b5..e2be8896c 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -3,7 +3,9 @@ import 'dart:math'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/video_actions.dart'; +import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/screen_on.dart'; @@ -11,11 +13,13 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/services/device_service.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/pedantic.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/tags_page.dart'; import 'package:collection/collection.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; final Settings settings = Settings._private(); @@ -47,6 +51,11 @@ class Settings extends ChangeNotifier { static const catalogTimeZoneKey = 'catalog_time_zone'; static const tileExtentPrefixKey = 'tile_extent_'; + // drawer + static const drawerTypeBookmarksKey = 'drawer_type_bookmarks'; + static const drawerAlbumBookmarksKey = 'drawer_album_bookmarks'; + static const drawerPageBookmarksKey = 'drawer_page_bookmarks'; + // collection static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; @@ -100,6 +109,16 @@ class Settings extends ChangeNotifier { static const lastVersionCheckDateKey = 'last_version_check_date'; // defaults + static final drawerTypeBookmarksDefault = [ + null, + MimeFilter.video, + FavouriteFilter.instance, + ]; + static final drawerPageBookmarksDefault = [ + AlbumListPage.routeName, + CountryListPage.routeName, + TagListPage.routeName, + ]; static const viewerQuickActionsDefault = [ EntryAction.toggleFavourite, EntryAction.share, @@ -209,6 +228,25 @@ class Settings extends ChangeNotifier { void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue); + // drawer + + List get drawerTypeBookmarks => + (_prefs!.getStringList(drawerTypeBookmarksKey))?.map((v) { + if (v.isEmpty) return null; + return CollectionFilter.fromJson(v); + }).toList() ?? + drawerTypeBookmarksDefault; + + set drawerTypeBookmarks(List newValue) => setAndNotify(drawerTypeBookmarksKey, newValue.map((filter) => filter?.toJson() ?? '').toList()); + + List? get drawerAlbumBookmarks => _prefs!.getStringList(drawerAlbumBookmarksKey); + + set drawerAlbumBookmarks(List? newValue) => setAndNotify(drawerAlbumBookmarksKey, newValue); + + List get drawerPageBookmarks => _prefs!.getStringList(drawerPageBookmarksKey) ?? drawerPageBookmarksDefault; + + set drawerPageBookmarks(List newValue) => setAndNotify(drawerPageBookmarksKey, newValue); + // collection EntryGroupFactor get collectionSectionFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values); @@ -447,7 +485,9 @@ class Settings extends ChangeNotifier { // apply user modifications jsonMap.forEach((key, value) { - if (key.startsWith(tileExtentPrefixKey)) { + if (value == null) { + _prefs!.remove(key); + } else if (key.startsWith(tileExtentPrefixKey)) { if (value is double) { _prefs!.setDouble(key, value); } else { @@ -511,6 +551,9 @@ class Settings extends ChangeNotifier { debugPrint('failed to import key=$key, value=$value is not a string'); } break; + case drawerTypeBookmarksKey: + case drawerAlbumBookmarksKey: + case drawerPageBookmarksKey: case pinnedFiltersKey: case hiddenFiltersKey: case viewerQuickActionsKey: diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 3075531d2..05f4c5d43 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -30,7 +30,7 @@ class AIcons { static const IconData tagOff = MdiIcons.tagOffOutline; // actions - static const IconData addPath = Icons.add_circle_outline; + static const IconData add = Icons.add_circle_outline; static const IconData addShortcut = Icons.add_to_home_screen_outlined; static const IconData replay10 = Icons.replay_10_outlined; static const IconData skip10 = Icons.forward_10_outlined; @@ -38,7 +38,6 @@ class AIcons { static const IconData clear = Icons.clear_outlined; static const IconData clipboard = Icons.content_copy_outlined; static const IconData copy = Icons.file_copy_outlined; - static const IconData createAlbum = Icons.add_circle_outline; static const IconData debug = Icons.whatshot_outlined; static const IconData delete = Icons.delete_outlined; static const IconData export = MdiIcons.fileExportOutline; @@ -70,6 +69,7 @@ class AIcons { static const IconData select = Icons.select_all_outlined; static const IconData setCover = MdiIcons.imageEditOutline; static const IconData share = Icons.share_outlined; + static const IconData show = Icons.visibility_outlined; static const IconData sort = Icons.sort_outlined; static const IconData speed = Icons.speed_outlined; static const IconData stats = Icons.pie_chart_outlined; diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index e61a7aa1b..c54138a13 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -15,7 +15,7 @@ class DebugSettingsSection extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, settings, child) { - String toMultiline(Iterable l) => l.isNotEmpty ? '\n${l.join('\n')}' : '$l'; + String toMultiline(Iterable? l) => l != null && l.isNotEmpty ? '\n${l.join('\n')}' : '$l'; return AvesExpansionTile( title: 'Settings', children: [ @@ -54,6 +54,9 @@ class DebugSettingsSection extends StatelessWidget { 'infoMapZoom': '${settings.infoMapZoom}', 'viewerQuickActions': '${settings.viewerQuickActions}', 'videoQuickActions': '${settings.videoQuickActions}', + 'drawerTypeBookmarks': toMultiline(settings.drawerTypeBookmarks), + 'drawerAlbumBookmarks': toMultiline(settings.drawerAlbumBookmarks), + 'drawerPageBookmarks': toMultiline(settings.drawerPageBookmarks), 'pinnedFilters': toMultiline(settings.pinnedFilters), 'hiddenFilters': toMultiline(settings.hiddenFilters), 'searchHistory': toMultiline(settings.searchHistory), diff --git a/lib/widgets/dialogs/cover_selection_dialog.dart b/lib/widgets/dialogs/cover_selection_dialog.dart index 54aa452be..4177ee42f 100644 --- a/lib/widgets/dialogs/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/cover_selection_dialog.dart @@ -77,9 +77,9 @@ class _CoverSelectionDialogState extends State { title, const Spacer(), IconButton( - onPressed: _isCustom ? _pickEntry : null, - tooltip: 'Change', icon: const Icon(AIcons.setCover), + onPressed: _isCustom ? _pickEntry : null, + tooltip: context.l10n.changeTooltip, ), ]) : title, diff --git a/lib/widgets/drawer/album_tile.dart b/lib/widgets/drawer/album_tile.dart deleted file mode 100644 index bcb41c34a..000000000 --- a/lib/widgets/drawer/album_tile.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:aves/model/filters/album.dart'; -import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/common/identity/aves_icons.dart'; -import 'package:aves/widgets/drawer/collection_tile.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class AlbumTile extends StatelessWidget { - final String album; - - const AlbumTile({ - Key? key, - required this.album, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final source = context.read(); - final displayName = source.getAlbumDisplayName(context, album); - return CollectionNavTile( - leading: IconUtils.getAlbumIcon( - context: context, - albumPath: album, - ), - title: displayName, - trailing: androidFileUtils.isOnRemovableStorage(album) - ? const Icon( - AIcons.removableStorage, - size: 16, - color: Colors.grey, - ) - : null, - filter: AlbumFilter(album, displayName), - ); - } -} diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 65cb80d1b..bdee8ebfc 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -1,7 +1,5 @@ import 'dart:ui'; -import 'package:aves/model/filters/favourite.dart'; -import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -16,8 +14,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; -import 'package:aves/widgets/drawer/album_tile.dart'; -import 'package:aves/widgets/drawer/collection_tile.dart'; +import 'package:aves/widgets/drawer/collection_nav_tile.dart'; +import 'package:aves/widgets/drawer/page_nav_tile.dart'; import 'package:aves/widgets/drawer/tile.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; @@ -32,6 +30,16 @@ class AppDrawer extends StatefulWidget { @override _AppDrawerState createState() => _AppDrawerState(); + + static List getDefaultAlbums(BuildContext context) { + final source = context.read(); + final specialAlbums = source.rawAlbums.where((album) { + final type = androidFileUtils.getAlbumType(album); + return [AlbumType.camera, AlbumType.screenshots].contains(type); + }).toList() + ..sort(source.compareAlbumsByName); + return specialAlbums; + } } class _AppDrawerState extends State { @@ -47,19 +55,11 @@ class _AppDrawerState extends State { @override Widget build(BuildContext context) { - final hiddenFilters = settings.hiddenFilters; - final showVideos = !hiddenFilters.contains(MimeFilter.video); - final showFavourites = !hiddenFilters.contains(FavouriteFilter.instance); final drawerItems = [ _buildHeader(context), - allCollectionTile, - if (showVideos) videoTile, - if (showFavourites) favouriteTile, - _buildSpecialAlbumSection(), - const Divider(), - albumListTile, - countryListTile, - tagListTile, + ..._buildTypeLinks(), + _buildAlbumLinks(), + ..._buildPageLinks(), if (!kReleaseMode) ...[ const Divider(), debugTile, @@ -192,82 +192,77 @@ class _AppDrawerState extends State { ); } - Widget _buildSpecialAlbumSection() { + List _buildTypeLinks() { + final hiddenFilters = settings.hiddenFilters; + final typeBookmarks = settings.drawerTypeBookmarks; + return typeBookmarks + .where((filter) => !hiddenFilters.contains(filter)) + .map((filter) => CollectionNavTile( + leading: DrawerFilterIcon(filter: filter), + title: DrawerFilterTitle(filter: filter), + filter: filter, + )) + .toList(); + } + + Widget _buildAlbumLinks() { return StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { - final specialAlbums = source.rawAlbums.where((album) { - final type = androidFileUtils.getAlbumType(album); - return [AlbumType.camera, AlbumType.screenshots].contains(type); - }).toList() - ..sort(source.compareAlbumsByName); - - if (specialAlbums.isEmpty) return const SizedBox.shrink(); + final albums = settings.drawerAlbumBookmarks ?? AppDrawer.getDefaultAlbums(context); + if (albums.isEmpty) return const SizedBox.shrink(); return Column( children: [ const Divider(), - ...specialAlbums.map((album) => AlbumTile(album: album)), + ...albums.map((album) => AlbumNavTile(album: album)), ], ); }); } - // tiles + List _buildPageLinks() { + final pageBookmarks = settings.drawerPageBookmarks; + if (pageBookmarks.isEmpty) return []; - Widget get allCollectionTile => CollectionNavTile( - leading: const Icon(AIcons.allCollection), - title: context.l10n.drawerCollectionAll, - filter: null, - ); + return [ + const Divider(), + ...pageBookmarks.map((route) { + WidgetBuilder? pageBuilder; + Widget? trailing; + switch (route) { + case AlbumListPage.routeName: + pageBuilder = (_) => const AlbumListPage(); + trailing = StreamBuilder( + stream: source.eventBus.on(), + builder: (context, _) => Text('${source.rawAlbums.length}'), + ); + break; + case CountryListPage.routeName: + pageBuilder = (_) => const CountryListPage(); + trailing = StreamBuilder( + stream: source.eventBus.on(), + builder: (context, _) => Text('${source.sortedCountries.length}'), + ); + break; + case TagListPage.routeName: + pageBuilder = (_) => const TagListPage(); + trailing = StreamBuilder( + stream: source.eventBus.on(), + builder: (context, _) => Text('${source.sortedTags.length}'), + ); + break; + } - Widget get videoTile => CollectionNavTile( - leading: const Icon(AIcons.video), - title: context.l10n.drawerCollectionVideos, - filter: MimeFilter.video, - ); + return PageNavTile( + trailing: trailing, + routeName: route, + pageBuilder: pageBuilder ?? (_) => const SizedBox(), + ); + }), + ]; + } - Widget get favouriteTile => CollectionNavTile( - leading: const Icon(AIcons.favourite), - title: context.l10n.drawerCollectionFavourites, - filter: FavouriteFilter.instance, - ); - - Widget get albumListTile => NavTile( - icon: AIcons.album, - title: context.l10n.albumPageTitle, - trailing: StreamBuilder( - stream: source.eventBus.on(), - builder: (context, _) => Text('${source.rawAlbums.length}'), - ), - routeName: AlbumListPage.routeName, - pageBuilder: (_) => const AlbumListPage(), - ); - - Widget get countryListTile => NavTile( - icon: AIcons.location, - title: context.l10n.countryPageTitle, - trailing: StreamBuilder( - stream: source.eventBus.on(), - builder: (context, _) => Text('${source.sortedCountries.length}'), - ), - routeName: CountryListPage.routeName, - pageBuilder: (_) => const CountryListPage(), - ); - - Widget get tagListTile => NavTile( - icon: AIcons.tag, - title: context.l10n.tagPageTitle, - trailing: StreamBuilder( - stream: source.eventBus.on(), - builder: (context, _) => Text('${source.sortedTags.length}'), - ), - routeName: TagListPage.routeName, - pageBuilder: (_) => const TagListPage(), - ); - - Widget get debugTile => NavTile( - icon: AIcons.debug, - title: 'Debug', + Widget get debugTile => PageNavTile( topLevel: false, routeName: AppDebugPage.routeName, pageBuilder: (_) => const AppDebugPage(), diff --git a/lib/widgets/drawer/collection_tile.dart b/lib/widgets/drawer/collection_nav_tile.dart similarity index 59% rename from lib/widgets/drawer/collection_tile.dart rename to lib/widgets/drawer/collection_nav_tile.dart index 761c9c529..7368f6483 100644 --- a/lib/widgets/drawer/collection_tile.dart +++ b/lib/widgets/drawer/collection_nav_tile.dart @@ -1,13 +1,17 @@ +import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/drawer/tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class CollectionNavTile extends StatelessWidget { final Widget? leading; - final String title; + final Widget title; final Widget? trailing; final bool dense; final CollectionFilter? filter; @@ -29,7 +33,7 @@ class CollectionNavTile extends StatelessWidget { bottom: false, child: ListTile( leading: leading, - title: Text(title), + title: title, trailing: trailing, dense: dense, onTap: () => _goToCollection(context), @@ -54,3 +58,30 @@ class CollectionNavTile extends StatelessWidget { ); } } + +class AlbumNavTile extends StatelessWidget { + final String album; + + const AlbumNavTile({ + Key? key, + required this.album, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final source = context.read(); + var filter = AlbumFilter(album, source.getAlbumDisplayName(context, album)); + return CollectionNavTile( + leading: DrawerFilterIcon(filter: filter), + title: DrawerFilterTitle(filter: filter), + trailing: androidFileUtils.isOnRemovableStorage(album) + ? const Icon( + AIcons.removableStorage, + size: 16, + color: Colors.grey, + ) + : null, + filter: filter, + ); + } +} diff --git a/lib/widgets/drawer/page_nav_tile.dart b/lib/widgets/drawer/page_nav_tile.dart new file mode 100644 index 000000000..fdf462655 --- /dev/null +++ b/lib/widgets/drawer/page_nav_tile.dart @@ -0,0 +1,64 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/drawer/tile.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class PageNavTile extends StatelessWidget { + final Widget? trailing; + final bool topLevel; + final String routeName; + final WidgetBuilder? pageBuilder; + + const PageNavTile({ + Key? key, + this.trailing, + this.topLevel = true, + required this.routeName, + required this.pageBuilder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final _pageBuilder = pageBuilder; + return SafeArea( + top: false, + bottom: false, + child: ListTile( + key: Key('$routeName-tile'), + leading: DrawerPageIcon(route: routeName), + title: DrawerPageTitle(route: routeName), + trailing: trailing != null + ? Builder( + builder: (context) => DefaultTextStyle.merge( + style: TextStyle( + color: IconTheme.of(context).color!.withOpacity(.6), + ), + child: trailing!, + ), + ) + : null, + onTap: _pageBuilder != null + ? () { + Navigator.pop(context); + if (routeName != context.currentRouteName) { + final route = MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: _pageBuilder, + ); + if (topLevel) { + Navigator.pushAndRemoveUntil( + context, + route, + (route) => false, + ); + } else { + Navigator.push(context, route); + } + } + } + : null, + selected: context.currentRouteName == routeName, + ), + ); + } +} diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index 3b250e79d..7ceefb16f 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -1,64 +1,111 @@ +import 'package:aves/model/filters/favourite.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/filters/type.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/foundation.dart'; +import 'package:aves/widgets/debug/app_debug_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/tags_page.dart'; import 'package:flutter/material.dart'; -class NavTile extends StatelessWidget { - final IconData icon; - final String title; - final Widget? trailing; - final bool topLevel; - final String routeName; - final WidgetBuilder pageBuilder; +class DrawerFilterIcon extends StatelessWidget { + final CollectionFilter? filter; - const NavTile({ + const DrawerFilterIcon({ Key? key, - required this.icon, - required this.title, - this.trailing, - this.topLevel = true, - required this.routeName, - required this.pageBuilder, + required this.filter, }) : super(key: key); @override Widget build(BuildContext context) { - return SafeArea( - top: false, - bottom: false, - child: ListTile( - key: Key('$title-tile'), - leading: Icon(icon), - title: Text(title), - trailing: trailing != null - ? Builder( - builder: (context) => DefaultTextStyle.merge( - style: TextStyle( - color: IconTheme.of(context).color!.withOpacity(.6), - ), - child: trailing!, - ), - ) - : null, - onTap: () { - Navigator.pop(context); - if (routeName != context.currentRouteName) { - final route = MaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: pageBuilder, - ); - if (topLevel) { - Navigator.pushAndRemoveUntil( - context, - route, - (route) => false, - ); - } else { - Navigator.push(context, route); - } - } - }, - selected: context.currentRouteName == routeName, - ), - ); + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final iconSize = 24 * textScaleFactor; + + final _filter = filter; + if (_filter == null) return Icon(AIcons.allCollection, size: iconSize); + return _filter.iconBuilder(context, iconSize) ?? const SizedBox(); + } +} + +class DrawerFilterTitle extends StatelessWidget { + final CollectionFilter? filter; + + const DrawerFilterTitle({ + Key? key, + required this.filter, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + String _getString(CollectionFilter? filter) { + final l10n = context.l10n; + if (filter == null) return l10n.drawerCollectionAll; + if (filter == FavouriteFilter.instance) return l10n.drawerCollectionFavourites; + if (filter == MimeFilter.image) return l10n.drawerCollectionImages; + if (filter == MimeFilter.video) return l10n.drawerCollectionVideos; + if (filter == TypeFilter.motionPhoto) return l10n.drawerCollectionMotionPhotos; + if (filter == TypeFilter.panorama) return l10n.drawerCollectionPanoramas; + if (filter == TypeFilter.sphericalVideo) return l10n.drawerCollectionSphericalVideos; + return filter.getLabel(context); + } + + return Text(_getString(filter)); + } +} + +class DrawerPageIcon extends StatelessWidget { + final String route; + + const DrawerPageIcon({ + Key? key, + required this.route, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + switch (route) { + case AlbumListPage.routeName: + return const Icon(AIcons.album); + case CountryListPage.routeName: + return const Icon(AIcons.location); + case TagListPage.routeName: + return const Icon(AIcons.tag); + case AppDebugPage.routeName: + return const Icon(AIcons.debug); + default: + return const SizedBox(); + } + } +} + +class DrawerPageTitle extends StatelessWidget { + final String route; + + const DrawerPageTitle({ + Key? key, + required this.route, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + String _getString() { + final l10n = context.l10n; + switch (route) { + case AlbumListPage.routeName: + return l10n.albumPageTitle; + case CountryListPage.routeName: + return l10n.countryPageTitle; + case TagListPage.routeName: + return l10n.tagPageTitle; + case AppDebugPage.routeName: + return 'Debug'; + default: + return route; + } + } + + return Text(_getString()); } } diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 714cadc13..b691a2a0c 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -29,7 +29,7 @@ class AlbumPickPage extends StatefulWidget { static const routeName = '/album_pick'; final CollectionSource source; - final MoveType moveType; + final MoveType? moveType; const AlbumPickPage({ Key? key, @@ -92,7 +92,7 @@ class _AlbumPickPageState extends State { class AlbumPickAppBar extends StatelessWidget { final CollectionSource source; - final MoveType moveType; + final MoveType? moveType; final AlbumChipSetActionDelegate actionDelegate; final ValueNotifier queryNotifier; @@ -117,7 +117,7 @@ class AlbumPickAppBar extends StatelessWidget { case MoveType.move: return context.l10n.albumPickPageTitleMove; default: - return moveType.toString(); + return context.l10n.albumPickPageTitlePick; } } @@ -131,19 +131,20 @@ class AlbumPickAppBar extends StatelessWidget { filterNotifier: queryNotifier, ), actions: [ - IconButton( - icon: const Icon(AIcons.createAlbum), - onPressed: () async { - final newAlbum = await showDialog( - context: context, - builder: (context) => const CreateAlbumDialog(), - ); - if (newAlbum != null && newAlbum.isNotEmpty) { - Navigator.pop(context, newAlbum); - } - }, - tooltip: context.l10n.createAlbumTooltip, - ), + if (moveType != null) + IconButton( + icon: const Icon(AIcons.add), + onPressed: () async { + final newAlbum = await showDialog( + context: context, + builder: (context) => const CreateAlbumDialog(), + ); + if (newAlbum != null && newAlbum.isNotEmpty) { + Navigator.pop(context, newAlbum); + } + }, + tooltip: context.l10n.createAlbumTooltip, + ), PopupMenuButton( itemBuilder: (context) { return [ diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart new file mode 100644 index 000000000..870852196 --- /dev/null +++ b/lib/widgets/settings/navigation/drawer.dart @@ -0,0 +1,133 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/drawer/app_drawer.dart'; +import 'package:aves/widgets/drawer/tile.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/tags_page.dart'; +import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:aves/widgets/settings/navigation/drawer_tab_albums.dart'; +import 'package:aves/widgets/settings/navigation/drawer_tab_fixed.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; + +class NavigationDrawerTile extends StatelessWidget { + const NavigationDrawerTile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(context.l10n.settingsNavigationDrawerTile), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: NavigationDrawerEditorPage.routeName), + builder: (context) => const NavigationDrawerEditorPage(), + ), + ); + }, + ); + } +} + +class NavigationDrawerEditorPage extends StatefulWidget { + static const routeName = '/settings/navigation_drawer'; + + const NavigationDrawerEditorPage({Key? key}) : super(key: key); + + @override + _NavigationDrawerEditorPageState createState() => _NavigationDrawerEditorPageState(); +} + +class _NavigationDrawerEditorPageState extends State { + final List _typeItems = []; + final Set _visibleTypes = {}; + final List _albumItems = []; + final List _pageItems = []; + final Set _visiblePages = {}; + + static final Set _typeOptions = { + null, + ...CollectionSearchDelegate.typeFilters, + }; + static const Set _pageOptions = { + AlbumListPage.routeName, + CountryListPage.routeName, + TagListPage.routeName, + }; + + @override + void initState() { + super.initState(); + final userTypeLinks = settings.drawerTypeBookmarks; + _visibleTypes.addAll(userTypeLinks); + _typeItems.addAll(userTypeLinks); + _typeItems.addAll(_typeOptions.where((v) => !userTypeLinks.contains(v))); + + _albumItems.addAll(settings.drawerAlbumBookmarks ?? AppDrawer.getDefaultAlbums(context)); + + final userPageLinks = settings.drawerPageBookmarks; + _visiblePages.addAll(userPageLinks); + _pageItems.addAll(userPageLinks); + _pageItems.addAll(_pageOptions.where((v) => !userPageLinks.contains(v))); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final tabs = >[ + Tuple2( + Tab(text: l10n.settingsNavigationDrawerTabTypes), + DrawerFixedListTab( + items: _typeItems, + visibleItems: _visibleTypes, + leading: (item) => DrawerFilterIcon(filter: item), + title: (item) => DrawerFilterTitle(filter: item), + ), + ), + Tuple2( + Tab(text: l10n.settingsNavigationDrawerTabAlbums), + DrawerAlbumTab( + items: _albumItems, + ), + ), + Tuple2( + Tab(text: l10n.settingsNavigationDrawerTabPages), + DrawerFixedListTab( + items: _pageItems, + visibleItems: _visiblePages, + leading: (item) => DrawerPageIcon(route: item), + title: (item) => DrawerPageTitle(route: item), + ), + ), + ]; + + return DefaultTabController( + length: tabs.length, + child: Scaffold( + appBar: AppBar( + title: Text(l10n.settingsNavigationDrawerEditorTitle), + bottom: TabBar( + tabs: tabs.map((t) => t.item1).toList(), + ), + ), + body: WillPopScope( + onWillPop: () { + settings.drawerTypeBookmarks = _typeItems.where(_visibleTypes.contains).toList(); + settings.drawerAlbumBookmarks = _albumItems; + settings.drawerPageBookmarks = _pageItems.where(_visiblePages.contains).toList(); + return SynchronousFuture(true); + }, + child: SafeArea( + child: TabBarView( + children: tabs.map((t) => t.item2).toList(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/settings/navigation/drawer_editor_banner.dart b/lib/widgets/settings/navigation/drawer_editor_banner.dart new file mode 100644 index 000000000..15b9a626e --- /dev/null +++ b/lib/widgets/settings/navigation/drawer_editor_banner.dart @@ -0,0 +1,22 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class DrawerEditorBanner extends StatelessWidget { + const DrawerEditorBanner({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Icon(AIcons.info), + const SizedBox(width: 16), + Expanded(child: Text(context.l10n.settingsNavigationDrawerBanner)), + ], + ), + ); + } +} diff --git a/lib/widgets/settings/navigation/drawer_tab_albums.dart b/lib/widgets/settings/navigation/drawer_tab_albums.dart new file mode 100644 index 000000000..4ee643695 --- /dev/null +++ b/lib/widgets/settings/navigation/drawer_tab_albums.dart @@ -0,0 +1,89 @@ +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/drawer/tile.dart'; +import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class DrawerAlbumTab extends StatefulWidget { + final List items; + + const DrawerAlbumTab({ + Key? key, + required this.items, + }) : super(key: key); + + @override + _DrawerAlbumTabState createState() => _DrawerAlbumTabState(); +} + +class _DrawerAlbumTabState extends State { + @override + Widget build(BuildContext context) { + final source = context.read(); + return Column( + children: [ + const DrawerEditorBanner(), + const Divider(height: 0), + Flexible( + child: ReorderableListView.builder( + itemBuilder: (context, index) { + final album = widget.items[index]; + final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album)); + return ListTile( + key: ValueKey(album), + leading: DrawerFilterIcon(filter: filter), + title: DrawerFilterTitle(filter: filter), + trailing: IconButton( + icon: const Icon(AIcons.clear), + onPressed: () { + setState(() => widget.items.remove(album)); + }, + tooltip: context.l10n.removeTooltip, + ), + ); + }, + itemCount: widget.items.length, + onReorder: (oldIndex, newIndex) { + setState(() { + if (oldIndex < newIndex) newIndex -= 1; + widget.items.insert(newIndex, widget.items.removeAt(oldIndex)); + }); + }, + shrinkWrap: true, + ), + ), + const Divider(height: 0), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: () async { + final source = context.read(); + final album = await Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: AlbumPickPage.routeName), + builder: (context) => AlbumPickPage(source: source, moveType: null), + ), + ); + + if (album == null || album.isEmpty) return; + + setState(() { + widget.items.add(album); + }); + }, + style: ButtonStyle( + side: MaterialStateProperty.all(BorderSide(color: Theme.of(context).accentColor)), + foregroundColor: MaterialStateProperty.all(Colors.white), + ), + icon: const Icon(AIcons.add), + label: Text(context.l10n.settingsNavigationDrawerAddAlbum), + ) + ], + ); + } +} diff --git a/lib/widgets/settings/navigation/drawer_tab_fixed.dart b/lib/widgets/settings/navigation/drawer_tab_fixed.dart new file mode 100644 index 000000000..78f965836 --- /dev/null +++ b/lib/widgets/settings/navigation/drawer_tab_fixed.dart @@ -0,0 +1,75 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +typedef ItemWidgetBuilder = Widget Function(T item); + +class DrawerFixedListTab extends StatefulWidget { + final List items; + final Set visibleItems; + final ItemWidgetBuilder leading; + final ItemWidgetBuilder title; + + const DrawerFixedListTab({ + Key? key, + required this.items, + required this.visibleItems, + required this.leading, + required this.title, + }) : super(key: key); + + @override + _DrawerFixedListTabState createState() => _DrawerFixedListTabState(); +} + +class _DrawerFixedListTabState extends State> { + Set get visibleItems => widget.visibleItems; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const DrawerEditorBanner(), + const Divider(height: 0), + Flexible( + child: ReorderableListView.builder( + itemBuilder: (context, index) { + final filter = widget.items[index]; + final visible = visibleItems.contains(filter); + return Opacity( + key: ValueKey(filter), + opacity: visible ? 1 : .4, + child: ListTile( + leading: widget.leading(filter), + title: widget.title(filter), + trailing: IconButton( + icon: Icon(visible ? AIcons.hide : AIcons.show), + onPressed: () { + setState(() { + if (visible) { + visibleItems.remove(filter); + } else { + visibleItems.add(filter); + } + }); + }, + tooltip: visible ? context.l10n.hideTooltip : context.l10n.showTooltip, + ), + ), + ); + }, + itemCount: widget.items.length, + onReorder: (oldIndex, newIndex) { + setState(() { + if (oldIndex < newIndex) newIndex -= 1; + widget.items.insert(newIndex, widget.items.removeAt(oldIndex)); + }); + }, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/settings/navigation.dart b/lib/widgets/settings/navigation/navigation.dart similarity index 96% rename from lib/widgets/settings/navigation.dart rename to lib/widgets/settings/navigation/navigation.dart index 9edd83c1d..50f8252fa 100644 --- a/lib/widgets/settings/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -8,6 +8,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/navigation/drawer.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -51,6 +52,7 @@ class NavigationSection extends StatelessWidget { } }, ), + const NavigationDrawerTile(), ListTile( title: Text(context.l10n.settingsKeepScreenOnTile), subtitle: Text(currentKeepScreenOn.getName(context)), diff --git a/lib/widgets/settings/privacy/hidden_paths.dart b/lib/widgets/settings/privacy/hidden_paths.dart index 5da86e9af..b2f44a10f 100644 --- a/lib/widgets/settings/privacy/hidden_paths.dart +++ b/lib/widgets/settings/privacy/hidden_paths.dart @@ -40,7 +40,7 @@ class HiddenPathPage extends StatelessWidget { title: Text(context.l10n.settingsHiddenPathsTitle), actions: [ IconButton( - icon: const Icon(AIcons.addPath), + icon: const Icon(AIcons.add), onPressed: () async { final path = await storageService.selectDirectory(); if (path != null && path.isNotEmpty) { @@ -87,7 +87,7 @@ class HiddenPathPage extends StatelessWidget { onPressed: () { context.read().changeFilterVisibility({pathFilter}, true); }, - tooltip: context.l10n.settingsHiddenPathsRemoveTooltip, + tooltip: context.l10n.removeTooltip, ), )), ], diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 6ca13f610..c57cea0f2 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -12,7 +12,7 @@ import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/settings/language/language.dart'; -import 'package:aves/widgets/settings/navigation.dart'; +import 'package:aves/widgets/settings/navigation/navigation.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart'; import 'package:aves/widgets/settings/thumbnails.dart'; import 'package:aves/widgets/settings/video/video.dart'; diff --git a/test_driver/app_test.dart b/test_driver/app_test.dart index c04a205d3..91709081e 100644 --- a/test_driver/app_test.dart +++ b/test_driver/app_test.dart @@ -122,12 +122,14 @@ void selectFirstAlbum() { await driver.tap(find.byValueKey('appbar-leading-button')); await driver.waitUntilNoTransientCallbacks(); - await driver.tap(find.byValueKey('Albums-tile')); + // prefix must match `AlbumListPage.routeName` + await driver.tap(find.byValueKey('/albums-tile')); await driver.waitUntilNoTransientCallbacks(); // wait for collection loading await driver.waitForCondition(const NoPendingPlatformMessages()); + // TODO TLAD fix finder await driver.tap(find.descendant( of: find.byValueKey('filter-grid-page'), matching: find.byType('CoveredFilterChip'),