#42 navigation menu customization

This commit is contained in:
Thibault Deckers 2021-08-14 18:36:10 +09:00
parent 690ebce80e
commit 8669f34bad
21 changed files with 720 additions and 204 deletions

View file

@ -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": {},

View file

@ -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": "저장공간 접근",

View file

@ -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;

View file

@ -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<CollectionFilter?> get drawerTypeBookmarks =>
(_prefs!.getStringList(drawerTypeBookmarksKey))?.map((v) {
if (v.isEmpty) return null;
return CollectionFilter.fromJson(v);
}).toList() ??
drawerTypeBookmarksDefault;
set drawerTypeBookmarks(List<CollectionFilter?> newValue) => setAndNotify(drawerTypeBookmarksKey, newValue.map((filter) => filter?.toJson() ?? '').toList());
List<String>? get drawerAlbumBookmarks => _prefs!.getStringList(drawerAlbumBookmarksKey);
set drawerAlbumBookmarks(List<String>? newValue) => setAndNotify(drawerAlbumBookmarksKey, newValue);
List<String> get drawerPageBookmarks => _prefs!.getStringList(drawerPageBookmarksKey) ?? drawerPageBookmarksDefault;
set drawerPageBookmarks(List<String> 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:

View file

@ -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;

View file

@ -15,7 +15,7 @@ class DebugSettingsSection extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<Settings>(
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),

View file

@ -77,9 +77,9 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
title,
const Spacer(),
IconButton(
onPressed: _isCustom ? _pickEntry : null,
tooltip: 'Change',
icon: const Icon(AIcons.setCover),
onPressed: _isCustom ? _pickEntry : null,
tooltip: context.l10n.changeTooltip,
),
])
: title,

View file

@ -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<CollectionSource>();
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),
);
}
}

View file

@ -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<String> getDefaultAlbums(BuildContext context) {
final source = context.read<CollectionSource>();
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<AppDrawer> {
@ -47,19 +55,11 @@ class _AppDrawerState extends State<AppDrawer> {
@override
Widget build(BuildContext context) {
final hiddenFilters = settings.hiddenFilters;
final showVideos = !hiddenFilters.contains(MimeFilter.video);
final showFavourites = !hiddenFilters.contains(FavouriteFilter.instance);
final drawerItems = <Widget>[
_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<AppDrawer> {
);
}
Widget _buildSpecialAlbumSection() {
List<Widget> _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<AlbumsChangedEvent>(),
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<Widget> _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<AlbumsChangedEvent>(),
builder: (context, _) => Text('${source.rawAlbums.length}'),
);
break;
case CountryListPage.routeName:
pageBuilder = (_) => const CountryListPage();
trailing = StreamBuilder(
stream: source.eventBus.on<CountriesChangedEvent>(),
builder: (context, _) => Text('${source.sortedCountries.length}'),
);
break;
case TagListPage.routeName:
pageBuilder = (_) => const TagListPage();
trailing = StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(),
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<AlbumsChangedEvent>(),
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<CountriesChangedEvent>(),
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<TagsChangedEvent>(),
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(),

View file

@ -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<CollectionSource>();
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,
);
}
}

View file

@ -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,
),
);
}
}

View file

@ -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());
}
}

View file

@ -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<AlbumPickPage> {
class AlbumPickAppBar extends StatelessWidget {
final CollectionSource source;
final MoveType moveType;
final MoveType? moveType;
final AlbumChipSetActionDelegate actionDelegate;
final ValueNotifier<String> 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<String>(
context: context,
builder: (context) => const CreateAlbumDialog(),
);
if (newAlbum != null && newAlbum.isNotEmpty) {
Navigator.pop<String>(context, newAlbum);
}
},
tooltip: context.l10n.createAlbumTooltip,
),
if (moveType != null)
IconButton(
icon: const Icon(AIcons.add),
onPressed: () async {
final newAlbum = await showDialog<String>(
context: context,
builder: (context) => const CreateAlbumDialog(),
);
if (newAlbum != null && newAlbum.isNotEmpty) {
Navigator.pop<String>(context, newAlbum);
}
},
tooltip: context.l10n.createAlbumTooltip,
),
PopupMenuButton<ChipSetAction>(
itemBuilder: (context) {
return [

View file

@ -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<NavigationDrawerEditorPage> {
final List<CollectionFilter?> _typeItems = [];
final Set<CollectionFilter?> _visibleTypes = {};
final List<String> _albumItems = [];
final List<String> _pageItems = [];
final Set<String> _visiblePages = {};
static final Set<CollectionFilter?> _typeOptions = {
null,
...CollectionSearchDelegate.typeFilters,
};
static const Set<String> _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, Widget>>[
Tuple2(
Tab(text: l10n.settingsNavigationDrawerTabTypes),
DrawerFixedListTab<CollectionFilter?>(
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<String>(
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(),
),
),
),
),
);
}
}

View file

@ -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)),
],
),
);
}
}

View file

@ -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<String> items;
const DrawerAlbumTab({
Key? key,
required this.items,
}) : super(key: key);
@override
_DrawerAlbumTabState createState() => _DrawerAlbumTabState();
}
class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
@override
Widget build(BuildContext context) {
final source = context.read<CollectionSource>();
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<CollectionSource>();
final album = await Navigator.push(
context,
MaterialPageRoute<String>(
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>(BorderSide(color: Theme.of(context).accentColor)),
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
),
icon: const Icon(AIcons.add),
label: Text(context.l10n.settingsNavigationDrawerAddAlbum),
)
],
);
}
}

View file

@ -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<T> = Widget Function(T item);
class DrawerFixedListTab<T> extends StatefulWidget {
final List<T> items;
final Set<T> visibleItems;
final ItemWidgetBuilder<T> leading;
final ItemWidgetBuilder<T> title;
const DrawerFixedListTab({
Key? key,
required this.items,
required this.visibleItems,
required this.leading,
required this.title,
}) : super(key: key);
@override
_DrawerFixedListTabState<T> createState() => _DrawerFixedListTabState<T>();
}
class _DrawerFixedListTabState<T> extends State<DrawerFixedListTab<T>> {
Set<T> 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));
});
},
),
),
],
);
}
}

View file

@ -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)),

View file

@ -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<CollectionSource>().changeFilterVisibility({pathFilter}, true);
},
tooltip: context.l10n.settingsHiddenPathsRemoveTooltip,
tooltip: context.l10n.removeTooltip,
),
)),
],

View file

@ -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';

View file

@ -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'),