This commit is contained in:
Thibault Deckers 2021-12-17 12:01:16 +09:00
parent 07183de5fb
commit 51ff287dcd
68 changed files with 1781 additions and 626 deletions

View file

@ -447,10 +447,8 @@
"genericFailureFeedback": "Failed", "genericFailureFeedback": "Failed",
"@genericFailureFeedback": {}, "@genericFailureFeedback": {},
"menuActionSort": "Sort", "menuActionConfigureView": "View",
"@menuActionSort": {}, "@menuActionConfigureView": {},
"menuActionGroup": "Group",
"@menuActionGroup": {},
"menuActionSelect": "Select", "menuActionSelect": "Select",
"@menuActionSelect": {}, "@menuActionSelect": {},
"menuActionSelectAll": "Select all", "menuActionSelectAll": "Select all",
@ -462,6 +460,18 @@
"menuActionStats": "Stats", "menuActionStats": "Stats",
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogTabSort": "Sort",
"@viewDialogTabSort": {},
"viewDialogTabGroup": "Group",
"@viewDialogTabGroup": {},
"viewDialogTabLayout": "Layout",
"@viewDialogTabLayout": {},
"tileLayoutGrid": "Grid",
"@tileLayoutGrid": {},
"tileLayoutList": "List",
"@tileLayoutList": {},
"aboutPageTitle": "About", "aboutPageTitle": "About",
"@aboutPageTitle": {}, "@aboutPageTitle": {},
"aboutLinkSources": "Sources", "aboutLinkSources": "Sources",
@ -566,8 +576,6 @@
"collectionSearchTitlesHintText": "Search titles", "collectionSearchTitlesHintText": "Search titles",
"@collectionSearchTitlesHintText": {}, "@collectionSearchTitlesHintText": {},
"collectionSortTitle": "Sort",
"@collectionSortTitle": {},
"collectionSortDate": "By date", "collectionSortDate": "By date",
"@collectionSortDate": {}, "@collectionSortDate": {},
"collectionSortSize": "By size", "collectionSortSize": "By size",
@ -575,8 +583,6 @@
"collectionSortName": "By album & file name", "collectionSortName": "By album & file name",
"@collectionSortName": {}, "@collectionSortName": {},
"collectionGroupTitle": "Group",
"@collectionGroupTitle": {},
"collectionGroupAlbum": "By album", "collectionGroupAlbum": "By album",
"@collectionGroupAlbum": {}, "@collectionGroupAlbum": {},
"collectionGroupMonth": "By month", "collectionGroupMonth": "By month",
@ -674,8 +680,6 @@
"drawerCollectionSphericalVideos": "360° Videos", "drawerCollectionSphericalVideos": "360° Videos",
"@drawerCollectionSphericalVideos": {}, "@drawerCollectionSphericalVideos": {},
"chipSortTitle": "Sort",
"@chipSortTitle": {},
"chipSortDate": "By date", "chipSortDate": "By date",
"@chipSortDate": {}, "@chipSortDate": {},
"chipSortName": "By name", "chipSortName": "By name",
@ -683,8 +687,6 @@
"chipSortCount": "By item count", "chipSortCount": "By item count",
"@chipSortCount": {}, "@chipSortCount": {},
"albumGroupTitle": "Group",
"@albumGroupTitle": {},
"albumGroupTier": "By tier", "albumGroupTier": "By tier",
"@albumGroupTier": {}, "@albumGroupTier": {},
"albumGroupVolume": "By storage volume", "albumGroupVolume": "By storage volume",

View file

@ -203,14 +203,20 @@
"genericSuccessFeedback": "Succès !", "genericSuccessFeedback": "Succès !",
"genericFailureFeedback": "Échec", "genericFailureFeedback": "Échec",
"menuActionSort": "Trier", "menuActionConfigureView": "Vue",
"menuActionGroup": "Grouper",
"menuActionSelect": "Sélectionner", "menuActionSelect": "Sélectionner",
"menuActionSelectAll": "Tout sélectionner", "menuActionSelectAll": "Tout sélectionner",
"menuActionSelectNone": "Tout désélectionner", "menuActionSelectNone": "Tout désélectionner",
"menuActionMap": "Carte", "menuActionMap": "Carte",
"menuActionStats": "Statistiques", "menuActionStats": "Statistiques",
"viewDialogTabSort": "Tri",
"viewDialogTabGroup": "Groupes",
"viewDialogTabLayout": "Vue",
"tileLayoutGrid": "Grille",
"tileLayoutList": "Liste",
"aboutPageTitle": "À propos", "aboutPageTitle": "À propos",
"aboutLinkSources": "Sources", "aboutLinkSources": "Sources",
"aboutLinkLicense": "Licence", "aboutLinkLicense": "Licence",
@ -261,12 +267,10 @@
"collectionSearchTitlesHintText": "Recherche de titres", "collectionSearchTitlesHintText": "Recherche de titres",
"collectionSortTitle": "Trier",
"collectionSortDate": "par date", "collectionSortDate": "par date",
"collectionSortSize": "par taille", "collectionSortSize": "par taille",
"collectionSortName": "alphabétiquement", "collectionSortName": "alphabétique",
"collectionGroupTitle": "Grouper",
"collectionGroupAlbum": "par album", "collectionGroupAlbum": "par album",
"collectionGroupMonth": "par mois", "collectionGroupMonth": "par mois",
"collectionGroupDay": "par jour", "collectionGroupDay": "par jour",
@ -302,12 +306,10 @@
"drawerCollectionRaws": "Photos Raw", "drawerCollectionRaws": "Photos Raw",
"drawerCollectionSphericalVideos": "Vidéos à 360°", "drawerCollectionSphericalVideos": "Vidéos à 360°",
"chipSortTitle": "Trier",
"chipSortDate": "par date", "chipSortDate": "par date",
"chipSortName": "par nom", "chipSortName": "alphabétique",
"chipSortCount": "par nombre déléments", "chipSortCount": "par nombre déléments",
"albumGroupTitle": "Grouper",
"albumGroupTier": "par importance", "albumGroupTier": "par importance",
"albumGroupVolume": "par volume de stockage", "albumGroupVolume": "par volume de stockage",
"albumGroupNone": "ne pas grouper", "albumGroupNone": "ne pas grouper",

View file

@ -203,14 +203,20 @@
"genericSuccessFeedback": "정상 처리됐습니다", "genericSuccessFeedback": "정상 처리됐습니다",
"genericFailureFeedback": "오류가 발생했습니다", "genericFailureFeedback": "오류가 발생했습니다",
"menuActionSort": "정렬", "menuActionConfigureView": "뷰 설정",
"menuActionGroup": "묶음",
"menuActionSelect": "선택", "menuActionSelect": "선택",
"menuActionSelectAll": "모두 선택", "menuActionSelectAll": "모두 선택",
"menuActionSelectNone": "모두 해제", "menuActionSelectNone": "모두 해제",
"menuActionMap": "지도", "menuActionMap": "지도",
"menuActionStats": "통계", "menuActionStats": "통계",
"viewDialogTabSort": "정렬",
"viewDialogTabGroup": "묶음",
"viewDialogTabLayout": "배치",
"tileLayoutGrid": "그리드",
"tileLayoutList": "목록",
"aboutPageTitle": "앱 정보", "aboutPageTitle": "앱 정보",
"aboutLinkSources": "소스 코드", "aboutLinkSources": "소스 코드",
"aboutLinkLicense": "라이선스", "aboutLinkLicense": "라이선스",
@ -261,12 +267,10 @@
"collectionSearchTitlesHintText": "제목 검색", "collectionSearchTitlesHintText": "제목 검색",
"collectionSortTitle": "정렬",
"collectionSortDate": "날짜", "collectionSortDate": "날짜",
"collectionSortSize": "크기", "collectionSortSize": "크기",
"collectionSortName": "이름", "collectionSortName": "이름",
"collectionGroupTitle": "묶음",
"collectionGroupAlbum": "앨범별로", "collectionGroupAlbum": "앨범별로",
"collectionGroupMonth": "월별로", "collectionGroupMonth": "월별로",
"collectionGroupDay": "날짜별로", "collectionGroupDay": "날짜별로",
@ -302,12 +306,10 @@
"drawerCollectionRaws": "Raw 이미지", "drawerCollectionRaws": "Raw 이미지",
"drawerCollectionSphericalVideos": "360° 동영상", "drawerCollectionSphericalVideos": "360° 동영상",
"chipSortTitle": "정렬",
"chipSortDate": "날짜", "chipSortDate": "날짜",
"chipSortName": "이름", "chipSortName": "이름",
"chipSortCount": "항목수", "chipSortCount": "항목수",
"albumGroupTitle": "묶음",
"albumGroupTier": "단계별로", "albumGroupTier": "단계별로",
"albumGroupVolume": "저장공간별로", "albumGroupVolume": "저장공간별로",
"albumGroupNone": "묶음 없음", "albumGroupNone": "묶음 없음",

View file

@ -203,14 +203,15 @@
"genericSuccessFeedback": "Выполнено!", "genericSuccessFeedback": "Выполнено!",
"genericFailureFeedback": "Не удалось", "genericFailureFeedback": "Не удалось",
"menuActionSort": "Сортировка",
"menuActionGroup": "Группировка",
"menuActionSelect": "Выбрать", "menuActionSelect": "Выбрать",
"menuActionSelectAll": "Выбрать все", "menuActionSelectAll": "Выбрать все",
"menuActionSelectNone": "Снять выделение", "menuActionSelectNone": "Снять выделение",
"menuActionMap": "Карта", "menuActionMap": "Карта",
"menuActionStats": "Статистика", "menuActionStats": "Статистика",
"viewDialogTabSort": "Сортировка",
"viewDialogTabGroup": "Группировка",
"aboutPageTitle": "О нас", "aboutPageTitle": "О нас",
"aboutLinkSources": "Исходники", "aboutLinkSources": "Исходники",
"aboutLinkLicense": "Лицензия", "aboutLinkLicense": "Лицензия",
@ -261,12 +262,10 @@
"collectionSearchTitlesHintText": "Поиск заголовков", "collectionSearchTitlesHintText": "Поиск заголовков",
"collectionSortTitle": "Сортировка",
"collectionSortDate": "По дате", "collectionSortDate": "По дате",
"collectionSortSize": "По размеру", "collectionSortSize": "По размеру",
"collectionSortName": "По имени альбома и файла", "collectionSortName": "По имени альбома и файла",
"collectionGroupTitle": "Группировка",
"collectionGroupAlbum": "По альбому", "collectionGroupAlbum": "По альбому",
"collectionGroupMonth": "По месяцу", "collectionGroupMonth": "По месяцу",
"collectionGroupDay": "По дню", "collectionGroupDay": "По дню",
@ -302,12 +301,10 @@
"drawerCollectionRaws": "RAW", "drawerCollectionRaws": "RAW",
"drawerCollectionSphericalVideos": "360° видео", "drawerCollectionSphericalVideos": "360° видео",
"chipSortTitle": "Сортировка",
"chipSortDate": "По дате", "chipSortDate": "По дате",
"chipSortName": "По названию", "chipSortName": "По названию",
"chipSortCount": "По количеству объектов", "chipSortCount": "По количеству объектов",
"albumGroupTitle": "Группировка",
"albumGroupTier": "По уровню", "albumGroupTier": "По уровню",
"albumGroupVolume": "По накопителю", "albumGroupVolume": "По накопителю",
"albumGroupNone": "Не группировать", "albumGroupNone": "Не группировать",

View file

@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
enum ChipSetAction { enum ChipSetAction {
// general // general
sort, configureView,
group,
select, select,
selectAll, selectAll,
selectNone, selectNone,
@ -27,8 +26,7 @@ enum ChipSetAction {
class ChipSetActions { class ChipSetActions {
static const general = [ static const general = [
ChipSetAction.sort, ChipSetAction.configureView,
ChipSetAction.group,
ChipSetAction.select, ChipSetAction.select,
ChipSetAction.selectAll, ChipSetAction.selectAll,
ChipSetAction.selectNone, ChipSetAction.selectNone,
@ -57,10 +55,8 @@ extension ExtraChipSetAction on ChipSetAction {
String getText(BuildContext context) { String getText(BuildContext context) {
switch (this) { switch (this) {
// general // general
case ChipSetAction.sort: case ChipSetAction.configureView:
return context.l10n.menuActionSort; return context.l10n.menuActionConfigureView;
case ChipSetAction.group:
return context.l10n.menuActionGroup;
case ChipSetAction.select: case ChipSetAction.select:
return context.l10n.menuActionSelect; return context.l10n.menuActionSelect;
case ChipSetAction.selectAll: case ChipSetAction.selectAll:
@ -101,10 +97,8 @@ extension ExtraChipSetAction on ChipSetAction {
IconData _getIconData() { IconData _getIconData() {
switch (this) { switch (this) {
// general // general
case ChipSetAction.sort: case ChipSetAction.configureView:
return AIcons.sort; return AIcons.view;
case ChipSetAction.group:
return AIcons.group;
case ChipSetAction.select: case ChipSetAction.select:
return AIcons.select; return AIcons.select;
case ChipSetAction.selectAll: case ChipSetAction.selectAll:

View file

@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
enum EntrySetAction { enum EntrySetAction {
// general // general
sort, configureView,
group,
select, select,
selectAll, selectAll,
selectNone, selectNone,
@ -32,8 +31,7 @@ enum EntrySetAction {
class EntrySetActions { class EntrySetActions {
static const general = [ static const general = [
EntrySetAction.sort, EntrySetAction.configureView,
EntrySetAction.group,
EntrySetAction.select, EntrySetAction.select,
EntrySetAction.selectAll, EntrySetAction.selectAll,
EntrySetAction.selectNone, EntrySetAction.selectNone,
@ -63,10 +61,8 @@ extension ExtraEntrySetAction on EntrySetAction {
String getText(BuildContext context) { String getText(BuildContext context) {
switch (this) { switch (this) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
return context.l10n.menuActionSort; return context.l10n.menuActionConfigureView;
case EntrySetAction.group:
return context.l10n.menuActionGroup;
case EntrySetAction.select: case EntrySetAction.select:
return context.l10n.menuActionSelect; return context.l10n.menuActionSelect;
case EntrySetAction.selectAll: case EntrySetAction.selectAll:
@ -119,10 +115,8 @@ extension ExtraEntrySetAction on EntrySetAction {
IconData _getIconData() { IconData _getIconData() {
switch (this) { switch (this) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
return AIcons.sort; return AIcons.view;
case EntrySetAction.group:
return AIcons.group;
case EntrySetAction.select: case EntrySetAction.select:
return AIcons.select; return AIcons.select;
case EntrySetAction.selectAll: case EntrySetAction.selectAll:

View file

@ -44,12 +44,11 @@ class AlbumFilter extends CollectionFilter {
String getTooltip(BuildContext context) => album; String getTooltip(BuildContext context) => album;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return IconUtils.getAlbumIcon( return IconUtils.getAlbumIcon(
context: context, context: context,
albumPath: album, albumPath: album,
size: size, size: size,
embossed: embossed,
) ?? ) ??
(showGenericIcon ? Icon(AIcons.album, size: size) : null); (showGenericIcon ? Icon(AIcons.album, size: size) : null);
} }

View file

@ -56,7 +56,7 @@ class CoordinateFilter extends CollectionFilter {
String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat); String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat);
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.geoBounds, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.geoBounds, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -29,7 +29,7 @@ class FavouriteFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel; String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.favourite, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size);
@override @override
Future<Color> color(BuildContext context) => SynchronousFuture(Colors.red); Future<Color> color(BuildContext context) => SynchronousFuture(Colors.red);

View file

@ -81,7 +81,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
String getTooltip(BuildContext context) => getLabel(context); String getTooltip(BuildContext context) => getLabel(context);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => null; Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null;
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context))); Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context)));

View file

@ -58,15 +58,13 @@ class LocationFilter extends CollectionFilter {
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location; String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
if (_countryCode != null && device.canRenderFlagEmojis) { if (_countryCode != null && device.canRenderFlagEmojis) {
final flag = countryCodeToFlag(_countryCode); final flag = countryCodeToFlag(_countryCode);
// as of Flutter v1.22.3, emoji shadows are rendered as colorful duplicates,
// not filled with the shadow color as expected, so we remove them
if (flag != null) { if (flag != null) {
return Text( return Text(
flag, flag,
style: TextStyle(fontSize: size, shadows: const []), style: TextStyle(fontSize: size),
textScaleFactor: 1.0, textScaleFactor: 1.0,
); );
} }

View file

@ -68,7 +68,7 @@ class MimeFilter extends CollectionFilter {
} }
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -64,7 +64,7 @@ class QueryFilter extends CollectionFilter {
String get universalLabel => query; String get universalLabel => query;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size);
@override @override
Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor); Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor);

View file

@ -44,7 +44,7 @@ class TagFilter extends CollectionFilter {
String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag; String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null;
@override @override
String get category => type; String get category => type;

View file

@ -94,7 +94,7 @@ class TypeFilter extends CollectionFilter {
} }
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -19,6 +19,7 @@ class SettingsDefaults {
static const mustBackTwiceToExit = true; static const mustBackTwiceToExit = true;
static const keepScreenOn = KeepScreenOn.viewerOnly; static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection; static const homePage = HomePageSetting.collection;
static const tileLayout = TileLayout.grid;
// drawer // drawer
static final drawerTypeBookmarks = [ static final drawerTypeBookmarks = [

View file

@ -49,6 +49,7 @@ class Settings extends ChangeNotifier {
static const homePageKey = 'home_page'; static const homePageKey = 'home_page';
static const catalogTimeZoneKey = 'catalog_time_zone'; static const catalogTimeZoneKey = 'catalog_time_zone';
static const tileExtentPrefixKey = 'tile_extent_'; static const tileExtentPrefixKey = 'tile_extent_';
static const tileLayoutPrefixKey = 'tile_layout_';
// drawer // drawer
static const drawerTypeBookmarksKey = 'drawer_type_bookmarks'; static const drawerTypeBookmarksKey = 'drawer_type_bookmarks';
@ -247,6 +248,10 @@ class Settings extends ChangeNotifier {
void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue); void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue);
TileLayout getTileLayout(String routeName) => getEnumOrDefault(tileLayoutPrefixKey + routeName, SettingsDefaults.tileLayout, TileLayout.values);
void setTileLayout(String routeName, TileLayout newValue) => setAndNotify(tileLayoutPrefixKey + routeName, newValue.toString());
// drawer // drawer
List<CollectionFilter?> get drawerTypeBookmarks => List<CollectionFilter?> get drawerTypeBookmarks =>
@ -570,6 +575,12 @@ class Settings extends ChangeNotifier {
} else { } else {
debugPrint('failed to import key=$key, value=$value is not a double'); debugPrint('failed to import key=$key, value=$value is not a double');
} }
} else if (key.startsWith(tileLayoutPrefixKey)) {
if (value is String) {
_prefs!.setString(key, value);
} else {
debugPrint('failed to import key=$key, value=$value is not a string');
}
} else { } else {
switch (key) { switch (key) {
case subtitleTextColorKey: case subtitleTextColorKey:

View file

@ -7,3 +7,5 @@ enum AlbumChipGroupFactor { none, importance, volume }
enum EntrySortFactor { date, size, name } enum EntrySortFactor { date, size, name }
enum EntryGroupFactor { none, album, month, day } enum EntryGroupFactor { none, album, month, day }
enum TileLayout { grid, list }

View file

@ -32,6 +32,11 @@ class AIcons {
static const IconData tag = Icons.local_offer_outlined; static const IconData tag = Icons.local_offer_outlined;
static const IconData tagOff = MdiIcons.tagOffOutline; static const IconData tagOff = MdiIcons.tagOffOutline;
// view
static const IconData group = Icons.group_work_outlined;
static const IconData layout = Icons.grid_view_outlined;
static const IconData sort = Icons.sort_outlined;
// actions // actions
static const IconData add = 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 addShortcut = Icons.add_to_home_screen_outlined;
@ -54,7 +59,6 @@ class AIcons {
static const IconData filterOff = MdiIcons.filterOffOutline; static const IconData filterOff = MdiIcons.filterOffOutline;
static const IconData geoBounds = Icons.public_outlined; static const IconData geoBounds = Icons.public_outlined;
static const IconData goUp = Icons.arrow_upward_outlined; static const IconData goUp = Icons.arrow_upward_outlined;
static const IconData group = Icons.group_work_outlined;
static const IconData hide = Icons.visibility_off_outlined; static const IconData hide = Icons.visibility_off_outlined;
static const IconData import = MdiIcons.fileImportOutline; static const IconData import = MdiIcons.fileImportOutline;
static const IconData info = Icons.info_outlined; static const IconData info = Icons.info_outlined;
@ -80,7 +84,6 @@ class AIcons {
static const IconData setCover = MdiIcons.imageEditOutline; static const IconData setCover = MdiIcons.imageEditOutline;
static const IconData share = Icons.share_outlined; static const IconData share = Icons.share_outlined;
static const IconData show = Icons.visibility_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 speed = Icons.speed_outlined;
static const IconData stats = Icons.pie_chart_outlined; static const IconData stats = Icons.pie_chart_outlined;
static const IconData streams = Icons.translate_outlined; static const IconData streams = Icons.translate_outlined;
@ -88,6 +91,7 @@ class AIcons {
static const IconData streamAudio = Icons.audiotrack_outlined; static const IconData streamAudio = Icons.audiotrack_outlined;
static const IconData streamText = Icons.closed_caption_outlined; static const IconData streamText = Icons.closed_caption_outlined;
static const IconData videoSettings = Icons.video_settings_outlined; static const IconData videoSettings = Icons.video_settings_outlined;
static const IconData view = Icons.grid_view_outlined;
static const IconData zoomIn = Icons.add_outlined; static const IconData zoomIn = Icons.add_outlined;
static const IconData zoomOut = Icons.remove_outlined; static const IconData zoomOut = Icons.remove_outlined;
static const IconData collapse = Icons.expand_less_outlined; static const IconData collapse = Icons.expand_less_outlined;

View file

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
class Constants { class Constants {
// as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped // as of Flutter v2.8.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
// so we give it a `strutStyle` with a slightly larger height // so we give it a `strutStyle` with a slightly larger height
static const overflowStrutStyle = StrutStyle(height: 1.3); static const overflowStrutStyle = StrutStyle(height: 1.3);

View file

@ -12,6 +12,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/collection/query_bar.dart'; import 'package:aves/widgets/collection/query_bar.dart';
@ -19,7 +20,7 @@ import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -395,11 +396,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Future<void> _onActionSelected(EntrySetAction action) async { Future<void> _onActionSelected(EntrySetAction action) async {
switch (action) { switch (action) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
await _sort(); await _configureView();
break;
case EntrySetAction.group:
await _group();
break; break;
case EntrySetAction.select: case EntrySetAction.select:
context.read<Selection<AvesEntry>>().select(); context.read<Selection<AvesEntry>>().select();
@ -434,47 +432,42 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
} }
Future<void> _sort() async { Future<void> _configureView() async {
final value = await showDialog<EntrySortFactor>( final initialValue = Tuple3(
context: context, settings.collectionSortFactor,
builder: (context) => AvesSelectionDialog<EntrySortFactor>( settings.collectionSectionFactor,
initialValue: settings.collectionSortFactor, settings.getTileLayout(CollectionPage.routeName),
options: {
EntrySortFactor.date: context.l10n.collectionSortDate,
EntrySortFactor.size: context.l10n.collectionSortSize,
EntrySortFactor.name: context.l10n.collectionSortName,
},
title: context.l10n.collectionSortTitle,
),
); );
// wait for the dialog to hide as applying the change may block the UI final value = await showDialog<Tuple3<EntrySortFactor?, EntryGroupFactor?, TileLayout?>>(
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
if (value != null) {
settings.collectionSortFactor = value;
}
}
Future<void> _group() async {
final value = await showDialog<EntryGroupFactor>(
context: context, context: context,
builder: (context) { builder: (context) {
final l10n = context.l10n; final l10n = context.l10n;
return AvesSelectionDialog<EntryGroupFactor>( return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
initialValue: settings.collectionSectionFactor, initialValue: initialValue,
options: { sortOptions: {
EntrySortFactor.date: l10n.collectionSortDate,
EntrySortFactor.size: l10n.collectionSortSize,
EntrySortFactor.name: l10n.collectionSortName,
},
groupOptions: {
EntryGroupFactor.album: l10n.collectionGroupAlbum, EntryGroupFactor.album: l10n.collectionGroupAlbum,
EntryGroupFactor.month: l10n.collectionGroupMonth, EntryGroupFactor.month: l10n.collectionGroupMonth,
EntryGroupFactor.day: l10n.collectionGroupDay, EntryGroupFactor.day: l10n.collectionGroupDay,
EntryGroupFactor.none: l10n.collectionGroupNone, EntryGroupFactor.none: l10n.collectionGroupNone,
}, },
title: l10n.collectionGroupTitle, layoutOptions: {
TileLayout.grid: l10n.tileLayoutGrid,
TileLayout.list: l10n.tileLayoutList,
},
); );
}, },
); );
// wait for the dialog to hide as applying the change may block the UI // wait for the dialog to hide as applying the change may block the UI
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
if (value != null) { if (value != null && initialValue != value) {
settings.collectionSectionFactor = value; settings.collectionSortFactor = value.item1!;
settings.collectionSectionFactor = value.item2!;
settings.setTileLayout(CollectionPage.routeName, value.item3!);
} }
} }

View file

@ -4,6 +4,7 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
@ -11,21 +12,22 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/app_bar.dart';
import 'package:aves/widgets/collection/draggable_thumb_label.dart'; import 'package:aves/widgets/collection/draggable_thumb_label.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/section_layout.dart';
import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/grid/tile.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/item_tracker.dart'; import 'package:aves/widgets/common/grid/item_tracker.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/grid/selector.dart'; import 'package:aves/widgets/common/grid/selector.dart';
import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/grid/sliver.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -74,11 +76,13 @@ class _CollectionGridContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
return Consumer<CollectionLens>( return Consumer<CollectionLens>(
builder: (context, collection, child) { builder: (context, collection, child) {
final sectionedListLayoutProvider = ValueListenableBuilder<double>( final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier), valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, tileExtent, child) { builder: (context, thumbnailExtent, child) {
return Selector<TileExtentController, Tuple3<double, int, double>>( return Selector<TileExtentController, Tuple3<double, int, double>>(
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
builder: (context, c, child) { builder: (context, c, child) {
@ -89,22 +93,27 @@ class _CollectionGridContent extends StatelessWidget {
final target = context.read<DurationsData>().staggeredAnimationPageTarget; final target = context.read<DurationsData>().staggeredAnimationPageTarget;
final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target); final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
return GridTheme( return GridTheme(
extent: tileExtent, extent: thumbnailExtent,
child: SectionedEntryListLayoutProvider( child: EntryListDetailsTheme(
collection: collection, extent: thumbnailExtent,
scrollableWidth: scrollableWidth, child: SectionedEntryListLayoutProvider(
columnCount: columnCount,
spacing: tileSpacing,
tileExtent: tileExtent,
tileBuilder: (entry) => InteractiveThumbnail(
key: ValueKey(entry.contentId),
collection: collection, collection: collection,
entry: entry, scrollableWidth: scrollableWidth,
tileExtent: tileExtent, tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier, columnCount: columnCount,
spacing: tileSpacing,
tileExtent: thumbnailExtent,
tileBuilder: (entry) => InteractiveTile(
key: ValueKey(entry.contentId),
collection: collection,
entry: entry,
thumbnailExtent: thumbnailExtent,
tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier,
),
tileAnimationDelay: tileAnimationDelay,
child: child!,
), ),
tileAnimationDelay: tileAnimationDelay,
child: child!,
), ),
); );
}, },
@ -115,6 +124,7 @@ class _CollectionGridContent extends StatelessWidget {
collection: collection, collection: collection,
isScrollingNotifier: _isScrollingNotifier, isScrollingNotifier: _isScrollingNotifier,
scrollController: PrimaryScrollController.of(context)!, scrollController: PrimaryScrollController.of(context)!,
tileLayout: tileLayout,
), ),
); );
return sectionedListLayoutProvider; return sectionedListLayoutProvider;
@ -127,27 +137,28 @@ class _CollectionSectionedContent extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final ValueNotifier<bool> isScrollingNotifier; final ValueNotifier<bool> isScrollingNotifier;
final ScrollController scrollController; final ScrollController scrollController;
final TileLayout tileLayout;
const _CollectionSectionedContent({ const _CollectionSectionedContent({
required this.collection, required this.collection,
required this.isScrollingNotifier, required this.isScrollingNotifier,
required this.scrollController, required this.scrollController,
required this.tileLayout,
}); });
@override @override
_CollectionSectionedContentState createState() => _CollectionSectionedContentState(); _CollectionSectionedContentState createState() => _CollectionSectionedContentState();
} }
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> with WidgetsBindingObserver, GridItemTrackerMixin<AvesEntry, _CollectionSectionedContent> { class _CollectionSectionedContentState extends State<_CollectionSectionedContent> {
CollectionLens get collection => widget.collection; CollectionLens get collection => widget.collection;
@override TileLayout get tileLayout => widget.tileLayout;
ScrollController get scrollController => widget.scrollController; ScrollController get scrollController => widget.scrollController;
@override
final ValueNotifier<double> appBarHeightNotifier = ValueNotifier(0); final ValueNotifier<double> appBarHeightNotifier = ValueNotifier(0);
@override
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
@override @override
@ -169,6 +180,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
final scaler = _CollectionScaler( final scaler = _CollectionScaler(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
appBarHeightNotifier: appBarHeightNotifier, appBarHeightNotifier: appBarHeightNotifier,
tileLayout: tileLayout,
child: scrollView, child: scrollView,
); );
@ -181,18 +193,26 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
child: scaler, child: scaler,
); );
return selector; return GridItemTracker<AvesEntry>(
scrollableKey: scrollableKey,
tileLayout: tileLayout,
appBarHeightNotifier: appBarHeightNotifier,
scrollController: scrollController,
child: selector,
);
} }
} }
class _CollectionScaler extends StatelessWidget { class _CollectionScaler extends StatelessWidget {
final GlobalKey scrollableKey; final GlobalKey scrollableKey;
final ValueNotifier<double> appBarHeightNotifier; final ValueNotifier<double> appBarHeightNotifier;
final TileLayout tileLayout;
final Widget child; final Widget child;
const _CollectionScaler({ const _CollectionScaler({
required this.scrollableKey, required this.scrollableKey,
required this.appBarHeightNotifier, required this.appBarHeightNotifier,
required this.tileLayout,
required this.child, required this.child,
}); });
@ -201,10 +221,12 @@ class _CollectionScaler extends StatelessWidget {
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing); final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
return GridScaleGestureDetector<AvesEntry>( return GridScaleGestureDetector<AvesEntry>(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
tileLayout: tileLayout,
heightForWidth: (width) => width, heightForWidth: (width) => width,
gridBuilder: (center, tileSize, child) => CustomPaint( gridBuilder: (center, tileSize, child) => CustomPaint(
painter: GridPainter( painter: GridPainter(
center: center, tileLayout: tileLayout,
tileCenter: center,
tileSize: tileSize, tileSize: tileSize,
spacing: tileSpacing, spacing: tileSpacing,
borderWidth: DecoratedThumbnail.borderWidth, borderWidth: DecoratedThumbnail.borderWidth,
@ -213,11 +235,13 @@ class _CollectionScaler extends StatelessWidget {
), ),
child: child, child: child,
), ),
scaledBuilder: (entry, tileSize) => DecoratedThumbnail( scaledBuilder: (entry, tileSize) => EntryListDetailsTheme(
entry: entry, extent: tileSize.height,
tileExtent: context.read<TileExtentController>().effectiveExtentMax, child: Tile(
selectable: false, entry: entry,
highlightable: false, thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
tileLayout: tileLayout,
),
), ),
child: child, child: child,
); );

View file

@ -50,10 +50,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
}) { }) {
switch (action) { switch (action) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
return true; return true;
case EntrySetAction.group:
return sortFactor == EntrySortFactor.date;
case EntrySetAction.select: case EntrySetAction.select:
return appMode.canSelect && !isSelecting; return appMode.canSelect && !isSelecting;
case EntrySetAction.selectAll: case EntrySetAction.selectAll:
@ -97,8 +95,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
final hasSelection = selectedItemCount > 0; final hasSelection = selectedItemCount > 0;
switch (action) { switch (action) {
case EntrySetAction.sort: case EntrySetAction.configureView:
case EntrySetAction.group:
return true; return true;
case EntrySetAction.select: case EntrySetAction.select:
return hasItems; return hasItems;
@ -132,8 +129,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
void onActionSelected(BuildContext context, EntrySetAction action) { void onActionSelected(BuildContext context, EntrySetAction action) {
switch (action) { switch (action) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
case EntrySetAction.group:
case EntrySetAction.select: case EntrySetAction.select:
case EntrySetAction.selectAll: case EntrySetAction.selectAll:
case EntrySetAction.selectNone: case EntrySetAction.selectNone:
@ -226,7 +222,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)), content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)),
actions: [ actions: [
TextButton( TextButton(
@ -274,19 +269,12 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
Future<void> _move(BuildContext context, {required MoveType moveType}) async { Future<void> _move(BuildContext context, {required MoveType moveType}) async {
final l10n = context.l10n; final l10n = context.l10n;
final source = context.read<CollectionSource>();
final selection = context.read<Selection<AvesEntry>>(); final selection = context.read<Selection<AvesEntry>>();
final selectedItems = _getExpandedSelectedItems(selection); final selectedItems = _getExpandedSelectedItems(selection);
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet(); final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
final destinationAlbum = await Navigator.push( final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
context, if (destinationAlbum == null) return;
MaterialPageRoute<String>(
settings: const RouteSettings(name: AlbumPickPage.routeName),
builder: (context) => AlbumPickPage(source: source, moveType: moveType),
),
);
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return; if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
@ -326,6 +314,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
nameConflictStrategy = value; nameConflictStrategy = value;
} }
final source = context.read<CollectionSource>();
source.pauseMonitoring(); source.pauseMonitoring();
final opId = mediaFileService.newOpId; final opId = mediaFileService.newOpId;
showOpReport<MoveOpEvent>( showOpReport<MoveOpEvent>(
@ -470,7 +459,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
builder: (context) { builder: (context) {
final l10n = context.l10n; final l10n = context.l10n;
return AvesDialog( return AvesDialog(
context: context,
title: l10n.unsupportedTypeDialogTitle, title: l10n.unsupportedTypeDialogTitle,
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))), content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
actions: [ actions: [

View file

@ -0,0 +1,95 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/coordinate_format.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class EntryListDetails extends StatelessWidget {
final AvesEntry entry;
const EntryListDetails({
Key? key,
required this.entry,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final detailsTheme = context.watch<EntryListDetailsThemeData>();
return Container(
padding: EntryListDetailsTheme.contentPadding,
foregroundDecoration: BoxDecoration(
border: Border(top: AvesBorder.side),
),
margin: EntryListDetailsTheme.contentMargin,
child: IconTheme.merge(
data: detailsTheme.iconTheme,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.bestTitle ?? context.l10n.viewerInfoUnknown,
style: detailsTheme.titleStyle,
softWrap: false,
overflow: detailsTheme.titleMaxLines == 1 ? TextOverflow.fade : TextOverflow.ellipsis,
maxLines: detailsTheme.titleMaxLines,
),
const SizedBox(height: EntryListDetailsTheme.titleDetailPadding),
if (detailsTheme.showDate) _buildDateRow(context, detailsTheme.captionStyle),
if (detailsTheme.showLocation && entry.hasGps) _buildLocationRow(context, detailsTheme.captionStyle),
],
),
),
);
}
Widget _buildDateRow(BuildContext context, TextStyle style) {
final locale = context.l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final date = entry.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown;
return Row(
children: [
const Icon(AIcons.date),
const SizedBox(width: 8),
Expanded(
child: Text(
dateText,
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
],
);
}
Widget _buildLocationRow(BuildContext context, TextStyle style) {
final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!);
return Row(
children: [
const Icon(AIcons.location),
const SizedBox(width: 8),
Expanded(
child: Text(
location,
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
],
);
}
}

View file

@ -0,0 +1,99 @@
import 'package:aves/theme/format.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
class EntryListDetailsTheme extends StatelessWidget {
final double extent;
final Widget child;
static const EdgeInsets contentMargin = EdgeInsets.symmetric(horizontal: 8);
static const EdgeInsets contentPadding = EdgeInsets.symmetric(vertical: 4);
static const double titleDetailPadding = 6;
const EntryListDetailsTheme({
Key? key,
required this.extent,
required this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ProxyProvider<MediaQueryData, EntryListDetailsThemeData>(
update: (context, mq, previous) {
final locale = context.l10n.localeName;
final use24hour = mq.alwaysUse24HourFormat;
final textScaleFactor = mq.textScaleFactor;
final textTheme = Theme.of(context).textTheme;
final titleStyle = textTheme.bodyText2!;
final captionStyle = textTheme.caption!;
final titleLineHeight = (RenderParagraph(
TextSpan(text: 'Fake Title', style: titleStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
final captionLineHeight = (RenderParagraph(
TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
strutStyle: Constants.overflowStrutStyle,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
var titleMaxLines = 1;
var showDate = false;
var showLocation = false;
var availableHeight = extent - contentMargin.vertical - contentPadding.vertical;
if (availableHeight >= titleLineHeight + titleDetailPadding + captionLineHeight) {
showDate = true;
availableHeight -= titleLineHeight + titleDetailPadding + captionLineHeight;
if (availableHeight >= captionLineHeight) {
showLocation = true;
availableHeight -= captionLineHeight;
titleMaxLines += availableHeight ~/ titleLineHeight;
}
}
return EntryListDetailsThemeData(
extent: extent,
titleMaxLines: titleMaxLines,
showDate: showDate,
showLocation: showLocation,
titleStyle: titleStyle,
captionStyle: captionStyle,
iconTheme: IconThemeData(
color: captionStyle.color,
size: captionStyle.fontSize! * textScaleFactor,
),
);
},
child: child,
);
}
}
class EntryListDetailsThemeData {
final double extent;
final int titleMaxLines;
final bool showDate, showLocation;
final TextStyle titleStyle, captionStyle;
final IconThemeData iconTheme;
const EntryListDetailsThemeData({
required this.extent,
required this.titleMaxLines,
required this.showDate,
required this.showLocation,
required this.titleStyle,
required this.captionStyle,
required this.iconTheme,
});
}

View file

@ -1,5 +1,6 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/widgets/collection/grid/headers/any.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
@ -12,6 +13,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
Key? key, Key? key,
required this.collection, required this.collection,
required double scrollableWidth, required double scrollableWidth,
required TileLayout tileLayout,
required int columnCount, required int columnCount,
required double spacing, required double spacing,
required double tileExtent, required double tileExtent,
@ -21,6 +23,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
}) : super( }) : super(
key: key, key: key,
scrollableWidth: scrollableWidth, scrollableWidth: scrollableWidth,
tileLayout: tileLayout,
columnCount: columnCount, columnCount: columnCount,
spacing: spacing, spacing: spacing,
tileWidth: tileExtent, tileWidth: tileExtent,

View file

@ -2,32 +2,36 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/services/viewer_service.dart'; import 'package:aves/services/viewer_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class InteractiveThumbnail extends StatelessWidget { class InteractiveTile extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
final AvesEntry entry; final AvesEntry entry;
final double tileExtent; final double thumbnailExtent;
final TileLayout tileLayout;
final ValueNotifier<bool>? isScrollingNotifier; final ValueNotifier<bool>? isScrollingNotifier;
const InteractiveThumbnail({ const InteractiveTile({
Key? key, Key? key,
required this.collection, required this.collection,
required this.entry, required this.entry,
required this.tileExtent, required this.thumbnailExtent,
required this.tileLayout,
this.isScrollingNotifier, this.isScrollingNotifier,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return InkWell(
key: ValueKey(entry.uri),
onTap: () { onTap: () {
final appMode = context.read<ValueNotifier<AppMode>>().value; final appMode = context.read<ValueNotifier<AppMode>>().value;
switch (appMode) { switch (appMode) {
@ -51,13 +55,13 @@ class InteractiveThumbnail extends StatelessWidget {
}, },
child: MetaData( child: MetaData(
metaData: ScalerMetadata(entry), metaData: ScalerMetadata(entry),
child: DecoratedThumbnail( child: Tile(
entry: entry, entry: entry,
tileExtent: tileExtent, thumbnailExtent: thumbnailExtent,
// when the user is scrolling faster than we can retrieve the thumbnails, tileLayout: tileLayout,
// the retrieval task queue can pile up for thumbnails that got disposed selectable: true,
// in this case we pause the image retrieval task to get it out of the queue highlightable: true,
cancellableNotifier: isScrollingNotifier, isScrollingNotifier: isScrollingNotifier,
// hero tag should include a collection identifier, so that it animates // hero tag should include a collection identifier, so that it animates
// between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // between different views of the entry in the same collection (e.g. thumbnails <-> viewer)
// but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer)
@ -86,3 +90,58 @@ class InteractiveThumbnail extends StatelessWidget {
); );
} }
} }
class Tile extends StatelessWidget {
final AvesEntry entry;
final double thumbnailExtent;
final TileLayout tileLayout;
final bool selectable, highlightable;
final ValueNotifier<bool>? isScrollingNotifier;
final Object? Function()? heroTagger;
const Tile({
Key? key,
required this.entry,
required this.thumbnailExtent,
required this.tileLayout,
this.selectable = false,
this.highlightable = false,
this.isScrollingNotifier,
this.heroTagger,
}) : super(key: key);
@override
Widget build(BuildContext context) {
switch (tileLayout) {
case TileLayout.grid:
return _buildThumbnail();
case TileLayout.list:
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox.square(
dimension: context.select<EntryListDetailsThemeData, double>((v) => v.extent),
child: _buildThumbnail(),
),
Expanded(
child: EntryListDetails(
entry: entry,
),
),
],
);
}
}
Widget _buildThumbnail() => DecoratedThumbnail(
entry: entry,
tileExtent: thumbnailExtent,
// when the user is scrolling faster than we can retrieve the thumbnails,
// the retrieval task queue can pile up for thumbnails that got disposed
// in this case we pause the image retrieval task to get it out of the queue
cancellableNotifier: isScrollingNotifier,
selectable: selectable,
highlightable: highlightable,
heroTagger: heroTagger,
);
}

View file

@ -55,7 +55,6 @@ mixin EntryEditorMixin {
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage), content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage),
actions: [ actions: [
TextButton( TextButton(

View file

@ -50,7 +50,6 @@ mixin PermissionAwareMixin {
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir); final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
final volume = dir.getVolumeDescription(context); final volume = dir.getVolumeDescription(context);
return AvesDialog( return AvesDialog(
context: context,
title: context.l10n.storageAccessDialogTitle, title: context.l10n.storageAccessDialogTitle,
content: Text(context.l10n.storageAccessDialogMessage(directory, volume)), content: Text(context.l10n.storageAccessDialogMessage(directory, volume)),
actions: [ actions: [
@ -84,7 +83,6 @@ mixin PermissionAwareMixin {
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir); final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
final volume = dir.getVolumeDescription(context); final volume = dir.getVolumeDescription(context);
return AvesDialog( return AvesDialog(
context: context,
title: context.l10n.restrictedAccessDialogTitle, title: context.l10n.restrictedAccessDialogTitle,
content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)), content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)),
actions: [ actions: [

View file

@ -80,7 +80,6 @@ mixin SizeAwareMixin {
final freeSize = formatFileSize(locale, free); final freeSize = formatFileSize(locale, free);
final volume = destinationVolume.getDescription(context); final volume = destinationVolume.getDescription(context);
return AvesDialog( return AvesDialog(
context: context,
title: l10n.notEnoughSpaceDialogTitle, title: l10n.notEnoughSpaceDialogTitle,
content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)),
actions: [ actions: [

View file

@ -71,7 +71,6 @@ class _ColorPickerDialogState extends State<ColorPickerDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesDialog( return AvesDialog(
context: context,
scrollableContent: [ scrollableContent: [
ColorPicker( ColorPicker(
color: color, color: color,

View file

@ -2,21 +2,45 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBindingObserver { class GridItemTracker<T> extends StatefulWidget {
ValueNotifier<double> get appBarHeightNotifier; final GlobalKey scrollableKey;
final ValueNotifier<double> appBarHeightNotifier;
final TileLayout tileLayout;
final ScrollController scrollController;
final Widget child;
ScrollController get scrollController; const GridItemTracker({
Key? key,
required this.scrollableKey,
required this.appBarHeightNotifier,
required this.tileLayout,
required this.scrollController,
required this.child,
}) : super(key: key);
GlobalKey get scrollableKey; @override
_GridItemTrackerState createState() => _GridItemTrackerState<T>();
}
class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBindingObserver {
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
ScrollController get scrollController => widget.scrollController;
@override
Widget build(BuildContext context) {
return widget.child;
}
Size get scrollableSize { Size get scrollableSize {
final scrollableContext = scrollableKey.currentContext!; final scrollableContext = widget.scrollableKey.currentContext!;
return (scrollableContext.findRenderObject() as RenderBox).size; return (scrollableContext.findRenderObject() as RenderBox).size;
} }
@ -43,11 +67,27 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
} }
@override @override
void didUpdateWidget(covariant oldWidget) { void didUpdateWidget(covariant GridItemTracker<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.tileLayout != widget.tileLayout) {
_onLayoutChange();
}
_saveLayoutMetrics(); _saveLayoutMetrics();
} }
@override
void didChangeMetrics() {
// the order of `WidgetsBindingObserver` metrics change notification is unreliable
// w.r.t. the `MediaQuery` update, and consequentially to this widget update:
// `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update
final orientation = _windowOrientation;
if (_lastOrientation != orientation) {
_lastOrientation = orientation;
_onLayoutChange();
_saveLayoutMetrics();
}
}
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance!.removeObserver(this); WidgetsBinding.instance!.removeObserver(this);
@ -95,18 +135,9 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
} }
} }
@override
void didChangeMetrics() {
final orientation = _windowOrientation;
if (_lastOrientation != orientation) {
_lastOrientation = orientation;
_onWindowOrientationChange();
}
}
Future<void> _saveLayoutMetrics() async { Future<void> _saveLayoutMetrics() async {
// use a delay to obtain current layout metrics // use a delay to obtain current layout metrics
// so that we can handle window orientation change beforehand with the previous metrics, // so that we can handle window orientation change with the previous metrics,
// regardless of the `MediaQuery`/`WidgetsBindingObserver` order uncertainty // regardless of the `MediaQuery`/`WidgetsBindingObserver` order uncertainty
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
@ -116,10 +147,10 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
} }
} }
// the order of `WidgetsBindingObserver` metrics change notification is unreliable void _onLayoutChange() {
// w.r.t. the `MediaQuery` update, and consequentially to this widget update // do not track when view shows top edge
// `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update if (scrollController.offset == 0) return;
void _onWindowOrientationChange() {
final layout = _lastSectionedListLayout; final layout = _lastSectionedListLayout;
final halfSize = _lastScrollableSize / 2; final halfSize = _lastScrollableSize / 2;
final center = Offset( final center = Offset(

View file

@ -1,6 +1,7 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart'; import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
@ -21,6 +22,7 @@ class ScalerMetadata<T> {
class GridScaleGestureDetector<T> extends StatefulWidget { class GridScaleGestureDetector<T> extends StatefulWidget {
final GlobalKey scrollableKey; final GlobalKey scrollableKey;
final TileLayout tileLayout;
final double Function(double width) heightForWidth; final double Function(double width) heightForWidth;
final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder; final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
final Widget Function(T item, Size tileSize) scaledBuilder; final Widget Function(T item, Size tileSize) scaledBuilder;
@ -30,6 +32,7 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
const GridScaleGestureDetector({ const GridScaleGestureDetector({
Key? key, Key? key,
required this.scrollableKey, required this.scrollableKey,
required this.tileLayout,
required this.heightForWidth, required this.heightForWidth,
required this.gridBuilder, required this.gridBuilder,
required this.scaledBuilder, required this.scaledBuilder,
@ -111,17 +114,29 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
_extentMax = tileExtentController.effectiveExtentMax; _extentMax = tileExtentController.effectiveExtentMax;
final halfSize = _startSize! / 2; final halfSize = _startSize! / 2;
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height)); final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (context) => ScaleOverlay( builder: (context) => _ScaleOverlay(
builder: (scaledTileSize) => SizedBox.fromSize( builder: (scaledTileSize) {
size: scaledTileSize, late final double themeExtent;
child: GridTheme( switch (widget.tileLayout) {
extent: scaledTileSize.width, case TileLayout.grid:
child: widget.scaledBuilder(_metadata!.item, scaledTileSize), themeExtent = scaledTileSize.width;
), break;
), case TileLayout.list:
center: thumbnailCenter, themeExtent = scaledTileSize.height;
break;
}
return SizedBox.fromSize(
size: scaledTileSize,
child: GridTheme(
extent: themeExtent,
child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
),
);
},
tileLayout: widget.tileLayout,
center: tileCenter,
viewportWidth: gridWidth, viewportWidth: gridWidth,
gridBuilder: widget.gridBuilder, gridBuilder: widget.gridBuilder,
scaledSizeNotifier: _scaledSizeNotifier!, scaledSizeNotifier: _scaledSizeNotifier!,
@ -133,8 +148,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
void _onScaleUpdate(ScaleUpdateDetails details) { void _onScaleUpdate(ScaleUpdateDetails details) {
if (_scaledSizeNotifier == null) return; if (_scaledSizeNotifier == null) return;
final s = details.scale; final s = details.scale;
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!); switch (widget.tileLayout) {
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth)); case TileLayout.grid:
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
break;
case TileLayout.list:
final scaledHeight = (_startSize!.height * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(_startSize!.width, scaledHeight);
break;
}
} }
void _onScaleEnd(ScaleEndDetails details) { void _onScaleEnd(ScaleEndDetails details) {
@ -148,7 +171,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
final tileExtentController = context.read<TileExtentController>(); final tileExtentController = context.read<TileExtentController>();
final oldExtent = tileExtentController.extentNotifier.value; final oldExtent = tileExtentController.extentNotifier.value;
// sanitize and update grid layout if necessary // sanitize and update grid layout if necessary
final newExtent = tileExtentController.setUserPreferredExtent(_scaledSizeNotifier!.value.width); late final double preferredExtent;
switch (widget.tileLayout) {
case TileLayout.grid:
preferredExtent = _scaledSizeNotifier!.value.width;
break;
case TileLayout.list:
preferredExtent = _scaledSizeNotifier!.value.height;
break;
}
final newExtent = tileExtentController.setUserPreferredExtent(preferredExtent);
_scaledSizeNotifier = null; _scaledSizeNotifier = null;
if (newExtent == oldExtent) { if (newExtent == oldExtent) {
_applyingScale = false; _applyingScale = false;
@ -183,16 +215,18 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
} }
} }
class ScaleOverlay extends StatefulWidget { class _ScaleOverlay extends StatefulWidget {
final Widget Function(Size scaledTileSize) builder; final Widget Function(Size scaledTileSize) builder;
final TileLayout tileLayout;
final Offset center; final Offset center;
final double viewportWidth; final double viewportWidth;
final ValueNotifier<Size> scaledSizeNotifier; final ValueNotifier<Size> scaledSizeNotifier;
final Widget Function(Offset center, Size extent, Widget child) gridBuilder; final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
const ScaleOverlay({ const _ScaleOverlay({
Key? key, Key? key,
required this.builder, required this.builder,
required this.tileLayout,
required this.center, required this.center,
required this.viewportWidth, required this.viewportWidth,
required this.scaledSizeNotifier, required this.scaledSizeNotifier,
@ -203,7 +237,7 @@ class ScaleOverlay extends StatefulWidget {
_ScaleOverlayState createState() => _ScaleOverlayState(); _ScaleOverlayState createState() => _ScaleOverlayState();
} }
class _ScaleOverlayState extends State<ScaleOverlay> { class _ScaleOverlayState extends State<_ScaleOverlay> {
bool _init = false; bool _init = false;
Offset get center => widget.center; Offset get center => widget.center;
@ -222,26 +256,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
child: Builder( child: Builder(
builder: (context) => IgnorePointer( builder: (context) => IgnorePointer(
child: AnimatedContainer( child: AnimatedContainer(
decoration: _init decoration: _buildBackgroundDecoration(context),
? BoxDecoration(
gradient: RadialGradient(
center: FractionalOffset.fromOffsetAndSize(center, context.select<MediaQueryData, Size>((mq) => mq.size)),
radius: 1,
colors: const [
Colors.black,
Colors.black54,
],
),
)
: const BoxDecoration(
// provide dummy gradient to lerp to the other one during animation
gradient: RadialGradient(
colors: [
Colors.transparent,
Colors.transparent,
],
),
),
duration: Durations.collectionScalingBackgroundAnimation, duration: Durations.collectionScalingBackgroundAnimation,
child: ValueListenableBuilder<Size>( child: ValueListenableBuilder<Size>(
valueListenable: widget.scaledSizeNotifier, valueListenable: widget.scaledSizeNotifier,
@ -281,17 +296,53 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
), ),
); );
} }
BoxDecoration _buildBackgroundDecoration(BuildContext context) {
late final Offset gradientCenter;
switch (widget.tileLayout) {
case TileLayout.grid:
gradientCenter = center;
break;
case TileLayout.list:
gradientCenter = Offset(0, center.dy);
break;
}
return _init
? BoxDecoration(
gradient: RadialGradient(
center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select<MediaQueryData, Size>((mq) => mq.size)),
radius: 1,
colors: const [
Colors.black,
Colors.black54,
// Colors.amber,
],
),
)
: const BoxDecoration(
// provide dummy gradient to lerp to the other one during animation
gradient: RadialGradient(
colors: [
Colors.transparent,
Colors.transparent,
],
),
);
}
} }
class GridPainter extends CustomPainter { class GridPainter extends CustomPainter {
final Offset center; final TileLayout tileLayout;
final Offset tileCenter;
final Size tileSize; final Size tileSize;
final double spacing, borderWidth; final double spacing, borderWidth;
final Radius borderRadius; final Radius borderRadius;
final Color color; final Color color;
const GridPainter({ const GridPainter({
required this.center, required this.tileLayout,
required this.tileCenter,
required this.tileSize, required this.tileSize,
required this.spacing, required this.spacing,
required this.borderWidth, required this.borderWidth,
@ -301,40 +352,73 @@ class GridPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final tileWidth = tileSize.width; late final Offset chipCenter;
final tileHeight = tileSize.height; late final Size chipSize;
late final int deltaColumn;
late final Shader strokeShader;
switch (tileLayout) {
case TileLayout.grid:
chipCenter = tileCenter;
chipSize = tileSize;
deltaColumn = 2;
strokeShader = ui.Gradient.radial(
tileCenter,
chipSize.shortestSide * 2,
[
color,
Colors.transparent,
],
[
.8,
1,
],
);
break;
case TileLayout.list:
chipSize = Size.square(tileSize.shortestSide);
chipCenter = Offset(chipSize.width / 2, tileCenter.dy);
deltaColumn = 0;
strokeShader = ui.Gradient.linear(
tileCenter - Offset(0, chipSize.shortestSide * 3),
tileCenter + Offset(0, chipSize.shortestSide * 3),
[
Colors.transparent,
color,
color,
Colors.transparent,
],
[
0,
.2,
.8,
1,
],
);
break;
}
final strokePaint = Paint() final strokePaint = Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = borderWidth ..strokeWidth = borderWidth
..shader = ui.Gradient.radial( ..shader = strokeShader;
center,
tileWidth * 2,
[
color,
Colors.transparent,
],
[
.8,
1,
],
);
final fillPaint = Paint() final fillPaint = Paint()
..style = PaintingStyle.fill ..style = PaintingStyle.fill
..color = color.withOpacity(.25); ..color = color.withOpacity(.25);
final deltaX = tileWidth + spacing; final chipWidth = chipSize.width;
final deltaY = tileHeight + spacing; final chipHeight = chipSize.height;
for (var i = -2; i <= 2; i++) {
final deltaX = tileSize.width + spacing;
final deltaY = tileSize.height + spacing;
for (var i = -deltaColumn; i <= deltaColumn; i++) {
final dx = deltaX * i; final dx = deltaX * i;
for (var j = -2; j <= 2; j++) { for (var j = -2; j <= 2; j++) {
if (i == 0 && j == 0) continue; if (i == 0 && j == 0) continue;
final dy = deltaY * j; final dy = deltaY * j;
final rect = RRect.fromRectAndRadius( final rect = RRect.fromRectAndRadius(
Rect.fromCenter( Rect.fromCenter(
center: center + Offset(dx, dy), center: chipCenter + Offset(dx, dy),
width: tileWidth, width: chipWidth - borderWidth,
height: tileHeight, height: chipHeight - borderWidth,
), ),
borderRadius, borderRadius,
); );

View file

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -12,6 +13,7 @@ import 'package:provider/provider.dart';
abstract class SectionedListLayoutProvider<T> extends StatelessWidget { abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final double scrollableWidth; final double scrollableWidth;
final TileLayout tileLayout;
final int columnCount; final int columnCount;
final double spacing, tileWidth, tileHeight; final double spacing, tileWidth, tileHeight;
final Widget Function(T item) tileBuilder; final Widget Function(T item) tileBuilder;
@ -21,14 +23,17 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
const SectionedListLayoutProvider({ const SectionedListLayoutProvider({
Key? key, Key? key,
required this.scrollableWidth, required this.scrollableWidth,
required this.columnCount, required this.tileLayout,
required int columnCount,
required this.spacing, required this.spacing,
required this.tileWidth, required double tileWidth,
required this.tileHeight, required this.tileHeight,
required this.tileBuilder, required this.tileBuilder,
required this.tileAnimationDelay, required this.tileAnimationDelay,
required this.child, required this.child,
}) : assert(scrollableWidth != 0), }) : assert(scrollableWidth != 0),
columnCount = tileLayout == TileLayout.list ? 1 : columnCount,
tileWidth = tileLayout == TileLayout.list ? scrollableWidth : tileWidth,
super(key: key); super(key: key);
@override @override

View file

@ -37,7 +37,7 @@ class AvesFilterDecoration {
class AvesFilterChip extends StatefulWidget { class AvesFilterChip extends StatefulWidget {
final CollectionFilter filter; final CollectionFilter filter;
final bool removable, showGenericIcon, useFilterColor; final bool removable, showText, showGenericIcon, useFilterColor;
final AvesFilterDecoration? decoration; final AvesFilterDecoration? decoration;
final String? banner; final String? banner;
final Widget? leadingOverride, details; final Widget? leadingOverride, details;
@ -60,6 +60,7 @@ class AvesFilterChip extends StatefulWidget {
Key? key, Key? key,
required this.filter, required this.filter,
this.removable = false, this.removable = false,
this.showText = true,
this.showGenericIcon = true, this.showGenericIcon = true,
this.useFilterColor = true, this.useFilterColor = true,
this.decoration, this.decoration,
@ -160,66 +161,70 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final chipBackground = Theme.of(context).scaffoldBackgroundColor;
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final iconSize = AvesFilterChip.iconSize * textScaleFactor;
final leading = widget.leadingOverride ?? filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon);
final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null;
final decoration = widget.decoration; final decoration = widget.decoration;
Widget content = Row( final chipBackground = Theme.of(context).scaffoldBackgroundColor;
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: const TextStyle(
fontSize: AvesFilterChip.fontSize,
),
softWrap: false,
overflow: TextOverflow.fade,
),
),
if (trailing != null) ...[
SizedBox(width: padding),
trailing,
],
],
);
final details = widget.details; Widget? content;
if (details != null) { if (widget.showText) {
content = Column( final textScaleFactor = MediaQuery.textScaleFactorOf(context);
mainAxisSize: MainAxisSize.min, final iconSize = AvesFilterChip.iconSize * textScaleFactor;
final leading = widget.leadingOverride ?? filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon);
final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null;
content = Row(
mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
content, if (leading != null) ...[
Flexible(child: details), leading,
SizedBox(width: padding),
],
Flexible(
child: Text(
filter.getLabel(context),
style: const TextStyle(
fontSize: AvesFilterChip.fontSize,
),
softWrap: false,
overflow: TextOverflow.fade,
),
),
if (trailing != null) ...[
SizedBox(width: padding),
trailing,
],
], ],
); );
}
if (decoration != null) { final details = widget.details;
content = Align( if (details != null) {
alignment: Alignment.bottomCenter, content = Column(
child: ClipRRect( mainAxisSize: MainAxisSize.min,
borderRadius: decoration.textBorderRadius, children: [
child: Container( content,
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: AvesFilterChip.decoratedContentVerticalPadding), Flexible(child: details),
color: chipBackground, ],
child: content, );
}
if (decoration != null) {
content = Align(
alignment: Alignment.bottomCenter,
child: ClipRRect(
borderRadius: decoration.textBorderRadius,
child: Container(
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: AvesFilterChip.decoratedContentVerticalPadding),
color: chipBackground,
child: content,
),
), ),
), );
); } else {
} else { content = Padding(
content = Padding( padding: EdgeInsets.symmetric(horizontal: padding * 2),
padding: EdgeInsets.symmetric(horizontal: padding * 2), child: content,
child: content, );
); }
} }
final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)); final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
@ -244,7 +249,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
child: InkWell( child: InkWell(
// as of Flutter v1.22.5, `InkWell` does not have `onLongPressStart` like `GestureDetector`, // as of Flutter v2.8.0, `InkWell` does not have `onLongPressStart` like `GestureDetector`,
// so we get the long press details from the tap instead // so we get the long press details from the tap instead
onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null, onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null,
onTap: onTap != null onTap: onTap != null

View file

@ -2,9 +2,7 @@ import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -201,25 +199,9 @@ class IconUtils {
required BuildContext context, required BuildContext context,
required String albumPath, required String albumPath,
double? size, double? size,
bool embossed = false,
}) { }) {
size ??= IconTheme.of(context).size; size ??= IconTheme.of(context).size;
Widget buildIcon(IconData icon) => embossed Widget buildIcon(IconData icon) => Icon(icon, size: size);
? MediaQuery(
// `DecoratedIcon` internally uses `Text`,
// which size depends on the ambient `textScaleFactor`
// but we already accommodate for it upstream
data: context.read<MediaQueryData>().copyWith(textScaleFactor: 1.0),
child: DecoratedIcon(
icon,
shadows: Constants.embossShadows,
size: size,
),
)
: Icon(
icon,
size: size,
);
switch (androidFileUtils.getAlbumType(albumPath)) { switch (androidFileUtils.getAlbumType(albumPath)) {
case AlbumType.camera: case AlbumType.camera:
return buildIcon(AIcons.cameraAlbum); return buildIcon(AIcons.cameraAlbum);

View file

@ -60,7 +60,6 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
final shortestSide = context.select<MediaQueryData, double>((mq) => mq.size.shortestSide); final shortestSide = context.select<MediaQueryData, double>((mq) => mq.size.shortestSide);
final extent = (shortestSide / 3.0).clamp(60.0, 160.0); final extent = (shortestSide / 3.0).clamp(60.0, 160.0);
return AvesDialog( return AvesDialog(
context: context,
scrollableContent: [ scrollableContent: [
if (_coverEntry != null) if (_coverEntry != null)
Container( Container(

View file

@ -3,64 +3,66 @@ import 'dart:ui';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AvesDialog extends AlertDialog { class AvesDialog extends StatelessWidget {
final String? title;
final ScrollController? scrollController;
final List<Widget>? scrollableContent;
final bool hasScrollBar;
final double horizontalContentPadding;
final Widget? content;
final List<Widget> actions;
static const double defaultHorizontalContentPadding = 24; static const double defaultHorizontalContentPadding = 24;
static const double controlCaptionPadding = 16; static const double controlCaptionPadding = 16;
static const double borderWidth = 1.0; static const double borderWidth = 1.0;
AvesDialog({ const AvesDialog({
Key? key, Key? key,
required BuildContext context, this.title,
String? title, this.scrollController,
ScrollController? scrollController, this.scrollableContent,
List<Widget>? scrollableContent, this.hasScrollBar = true,
bool hasScrollBar = true, this.horizontalContentPadding = defaultHorizontalContentPadding,
double horizontalContentPadding = defaultHorizontalContentPadding, this.content,
Widget? content, required this.actions,
required List<Widget> actions,
}) : assert((scrollableContent != null) ^ (content != null)), }) : assert((scrollableContent != null) ^ (content != null)),
super( super(key: key);
key: key,
title: title != null
? Padding(
// padding to avoid transparent border overlapping
padding: const EdgeInsets.symmetric(horizontal: borderWidth),
child: DialogTitle(title: title),
)
: null,
titlePadding: EdgeInsets.zero,
// the `scrollable` flag of `AlertDialog` makes it
// scroll both the title and the content together,
// and overflow feedback ignores the dialog shape,
// so we restrict scrolling to the content instead
content: _buildContent(context, scrollController, scrollableContent, hasScrollBar, content),
contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.fromLTRB(horizontalContentPadding, 20, horizontalContentPadding, 0),
actions: actions,
actionsPadding: const EdgeInsets.symmetric(horizontal: 8),
shape: RoundedRectangleBorder(
side: Divider.createBorderSide(context, width: borderWidth),
borderRadius: const BorderRadius.all(Radius.circular(24)),
),
);
static Widget _buildContent( @override
BuildContext context, Widget build(BuildContext context) {
ScrollController? scrollController, return AlertDialog(
List<Widget>? scrollableContent, title: title != null
bool hasScrollBar, ? Padding(
Widget? content, // padding to avoid transparent border overlapping
) { padding: const EdgeInsets.symmetric(horizontal: borderWidth),
child: DialogTitle(title: title!),
)
: null,
titlePadding: EdgeInsets.zero,
// the `scrollable` flag of `AlertDialog` makes it
// scroll both the title and the content together,
// and overflow feedback ignores the dialog shape,
// so we restrict scrolling to the content instead
content: _buildContent(context),
contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.fromLTRB(horizontalContentPadding, 20, horizontalContentPadding, 0),
actions: actions,
actionsPadding: const EdgeInsets.symmetric(horizontal: 8),
shape: shape(context),
);
}
Widget _buildContent(BuildContext context) {
if (content != null) { if (content != null) {
return content; return content!;
} }
if (scrollableContent != null) { if (scrollableContent != null) {
scrollController ??= ScrollController(); final _scrollController = scrollController ?? ScrollController();
Widget child = ListView( Widget child = ListView(
controller: scrollController, controller: _scrollController,
shrinkWrap: true, shrinkWrap: true,
children: scrollableContent, children: scrollableContent!,
); );
if (hasScrollBar) { if (hasScrollBar) {
@ -75,7 +77,7 @@ class AvesDialog extends AlertDialog {
), ),
), ),
child: Scrollbar( child: Scrollbar(
controller: scrollController, controller: _scrollController,
child: child, child: child,
), ),
); );
@ -89,11 +91,7 @@ class AvesDialog extends AlertDialog {
// but the `ListView` viewport does not have one // but the `ListView` viewport does not have one
width: 1, width: 1,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: contentDecoration(context),
border: Border(
bottom: Divider.createBorderSide(context, width: borderWidth),
),
),
child: child, child: child,
), ),
); );
@ -101,6 +99,21 @@ class AvesDialog extends AlertDialog {
return const SizedBox(); return const SizedBox();
} }
static Decoration contentDecoration(BuildContext context) => BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, width: borderWidth),
),
);
static const Radius cornerRadius = Radius.circular(24);
static ShapeBorder shape(BuildContext context) {
return RoundedRectangleBorder(
side: Divider.createBorderSide(context, width: borderWidth),
borderRadius: const BorderRadius.all(cornerRadius),
);
}
} }
class DialogTitle extends StatelessWidget { class DialogTitle extends StatelessWidget {
@ -116,11 +129,7 @@ class DialogTitle extends StatelessWidget {
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
decoration: BoxDecoration( decoration: AvesDialog.contentDecoration(context),
border: Border(
bottom: Divider.createBorderSide(context, width: AvesDialog.borderWidth),
),
),
child: Text( child: Text(
title, title,
style: const TextStyle( style: const TextStyle(
@ -138,7 +147,6 @@ void showNoMatchingAppDialog(BuildContext context) {
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
title: context.l10n.noMatchingAppDialogTitle, title: context.l10n.noMatchingAppDialogTitle,
content: Text(context.l10n.noMatchingAppDialogMessage), content: Text(context.l10n.noMatchingAppDialogMessage),
actions: [ actions: [

View file

@ -40,7 +40,6 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
final confirmationButtonLabel = widget.confirmationButtonLabel; final confirmationButtonLabel = widget.confirmationButtonLabel;
final needConfirmation = confirmationButtonLabel != null; final needConfirmation = confirmationButtonLabel != null;
return AvesDialog( return AvesDialog(
context: context,
title: widget.title, title: widget.title,
scrollableContent: [ scrollableContent: [
if (message != null) if (message != null)

View file

@ -129,7 +129,6 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
), ),
), ),
child: AvesDialog( child: AvesDialog(
context: context,
title: l10n.editEntryDateDialogTitle, title: l10n.editEntryDateDialogTitle,
scrollableContent: [ scrollableContent: [
setTile, setTile,
@ -289,7 +288,6 @@ class _TimeShiftDialogState extends State<TimeShiftDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const textStyle = TextStyle(fontSize: 34); const textStyle = TextStyle(fontSize: 34);
return AvesDialog( return AvesDialog(
context: context,
scrollableContent: [ scrollableContent: [
Center( Center(
child: Padding( child: Padding(

View file

@ -46,7 +46,6 @@ class _RemoveEntryMetadataDialogState extends State<RemoveEntryMetadataDialog> {
final l10n = context.l10n; final l10n = context.l10n;
final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation); final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation);
return AvesDialog( return AvesDialog(
context: context,
title: l10n.removeEntryMetadataDialogTitle, title: l10n.removeEntryMetadataDialogTitle,
scrollableContent: [ scrollableContent: [
..._mainOptions.map(_toTile), ..._mainOptions.map(_toTile),

View file

@ -41,7 +41,6 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesDialog( return AvesDialog(
context: context,
content: TextField( content: TextField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(

View file

@ -33,7 +33,6 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesDialog( return AvesDialog(
context: context,
content: Row( content: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View file

@ -4,11 +4,9 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.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/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/item_pick_dialog.dart'; import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -29,14 +27,13 @@ class CoverSelectionDialog extends StatefulWidget {
class _CoverSelectionDialogState extends State<CoverSelectionDialog> { class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
late bool _isCustom; late bool _isCustom;
AvesEntry? _customEntry, _recentEntry; AvesEntry? _customEntry;
CollectionFilter get filter => widget.filter; CollectionFilter get filter => widget.filter;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_recentEntry = context.read<CollectionSource>().recentEntry(filter);
_customEntry = widget.customEntry; _customEntry = widget.customEntry;
_isCustom = _customEntry != null; _isCustom = _customEntry != null;
} }
@ -47,10 +44,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
child: Builder( child: Builder(
builder: (context) { builder: (context) {
final l10n = context.l10n; final l10n = context.l10n;
final shortestSide = context.select<MediaQueryData, double>((mq) => mq.size.shortestSide);
final extent = (shortestSide / 3.0).clamp(60.0, 160.0);
return AvesDialog( return AvesDialog(
context: context,
title: l10n.setCoverDialogTitle, title: l10n.setCoverDialogTitle,
scrollableContent: [ scrollableContent: [
...[false, true].map( ...[false, true].map(
@ -89,17 +83,6 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
); );
}, },
), ),
Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(bottom: 16),
child: CoveredFilterChip(
filter: filter,
extent: extent,
coverEntry: _isCustom ? _customEntry : _recentEntry,
onTap: (filter) => _pickEntry(),
heroType: HeroType.never,
),
),
], ],
actions: [ actions: [
TextButton( TextButton(

View file

@ -63,7 +63,6 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
} }
return AvesDialog( return AvesDialog(
context: context,
title: context.l10n.newAlbumDialogTitle, title: context.l10n.newAlbumDialogTitle,
scrollController: _scrollController, scrollController: _scrollController,
scrollableContent: [ scrollableContent: [

View file

@ -42,7 +42,6 @@ class _RenameAlbumDialogState extends State<RenameAlbumDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesDialog( return AvesDialog(
context: context,
content: ValueListenableBuilder<bool>( content: ValueListenableBuilder<bool>(
valueListenable: _existsNotifier, valueListenable: _existsNotifier,
builder: (context, exists, child) { builder: (context, exists, child) {

View file

@ -0,0 +1,287 @@
import 'dart:math';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:tuple/tuple.dart';
import 'aves_dialog.dart';
class TileViewDialog<S, G, L> extends StatefulWidget {
final Tuple3<S?, G?, L?> initialValue;
final Map<S, String> sortOptions;
final Map<G, String> groupOptions;
final Map<L, String> layoutOptions;
const TileViewDialog({
Key? key,
required this.initialValue,
this.sortOptions = const {},
this.groupOptions = const {},
this.layoutOptions = const {},
}) : super(key: key);
@override
_TileViewDialogState createState() => _TileViewDialogState<S, G, L>();
}
class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with SingleTickerProviderStateMixin {
late S? _selectedSort;
late G? _selectedGroup;
late L? _selectedLayout;
late final TabController _tabController;
late final String _optionLines;
Map<S, String> get sortOptions => widget.sortOptions;
Map<G, String> get groupOptions => widget.groupOptions;
Map<L, String> get layoutOptions => widget.layoutOptions;
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
static const double tabIndicatorWeight = 2;
@override
void initState() {
super.initState();
final initialValue = widget.initialValue;
_selectedSort = initialValue.item1;
_selectedGroup = initialValue.item2;
_selectedLayout = initialValue.item3;
final allOptions = [
sortOptions,
groupOptions,
layoutOptions,
];
final tabCount = allOptions.where((options) => options.isNotEmpty).length;
_tabController = TabController(length: tabCount, vsync: this);
_tabController.addListener(_onTabChange);
_optionLines = allOptions.expand((v) => v.values).fold('', (previousValue, element) => '$previousValue\n$element');
}
@override
void dispose() {
_tabController.removeListener(_onTabChange);
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final tabs = <Tuple2<Tab, Widget>>[
if (sortOptions.isNotEmpty)
Tuple2(
_buildTab(context, AIcons.sort, l10n.viewDialogTabSort),
Column(
children: sortOptions.entries
.map((kv) => _buildRadioListTile<S>(
kv.key,
kv.value,
() => _selectedSort,
(v) => _selectedSort = v,
))
.toList(),
),
),
if (groupOptions.isNotEmpty)
Tuple2(
_buildTab(context, AIcons.group, l10n.viewDialogTabGroup, color: canGroup ? null : Theme.of(context).disabledColor),
Column(
children: groupOptions.entries
.map((kv) => _buildRadioListTile<G>(
kv.key,
kv.value,
() => _selectedGroup,
(v) => _selectedGroup = v,
))
.toList(),
),
),
if (layoutOptions.isNotEmpty)
Tuple2(
_buildTab(context, AIcons.layout, l10n.viewDialogTabLayout),
Column(
children: layoutOptions.entries
.map((kv) => _buildRadioListTile<L>(
kv.key,
kv.value,
() => _selectedLayout,
(v) => _selectedLayout = v,
))
.toList(),
),
),
];
final contentWidget = DecoratedBox(
decoration: AvesDialog.contentDecoration(context),
child: LayoutBuilder(
builder: (context, constraints) {
final availableBodyHeight = constraints.maxHeight - tabBarHeight(context) - tabIndicatorWeight;
final maxHeight = min(availableBodyHeight, tabBodyMaxHeight(context));
return Column(
mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Material(
borderRadius: const BorderRadius.only(
topLeft: AvesDialog.cornerRadius,
topRight: AvesDialog.cornerRadius,
),
clipBehavior: Clip.antiAlias,
child: TabBar(
indicatorWeight: tabIndicatorWeight,
tabs: tabs.map((t) => t.item1).toList(),
controller: _tabController,
),
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
),
child: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: tabs
.map((t) => SingleChildScrollView(
child: t.item2,
))
.toList(),
),
),
],
);
},
),
);
const actionsPadding = EdgeInsets.symmetric(horizontal: 8);
const double actionsSpacing = 8.0;
final actionsWidget = Padding(
padding: actionsPadding.add(const EdgeInsets.all(actionsSpacing)),
child: OverflowBar(
alignment: MainAxisAlignment.end,
spacing: actionsSpacing,
overflowAlignment: OverflowBarAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
onPressed: () => Navigator.pop(context, Tuple3(_selectedSort, _selectedGroup, _selectedLayout)),
child: Text(l10n.applyButtonLabel),
)
],
),
);
Widget dialogChild = LayoutBuilder(
builder: (context, constraints) {
final availableBodyWidth = constraints.maxWidth;
final maxWidth = min(availableBodyWidth, tabBodyMaxWidth(context));
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxWidth,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(child: contentWidget),
actionsWidget,
],
),
);
},
);
return Dialog(
shape: AvesDialog.shape(context),
child: dialogChild,
);
}
Tab _buildTab(BuildContext context, IconData icon, String text, {Color? color}) {
// cannot use `IconTheme` over `TabBar` to change size,
// because `TabBar` does so internally
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final iconSize = IconTheme.of(context).size! * textScaleFactor;
return Tab(
height: tabBarHeight(context),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: iconSize,
color: color,
),
const SizedBox(height: 4),
Text(
text,
style: TextStyle(color: color),
softWrap: false,
overflow: TextOverflow.fade,
),
],
),
);
}
bool get canGroup => _selectedSort == EntrySortFactor.date || _selectedSort is ChipSortFactor;
void _onTabChange() {
if (!canGroup && _tabController.index == 1) {
_tabController.index = _tabController.previousIndex;
}
}
// based on `ListTile` height computation (one line, no subtitle, not dense)
double singleOptionTileHeight(BuildContext context) => 56.0 + Theme.of(context).visualDensity.baseSizeAdjustment.dy;
double tabBodyMaxWidth(BuildContext context) {
final para = RenderParagraph(
TextSpan(text: _optionLines, style: Theme.of(context).textTheme.subtitle1!),
textDirection: TextDirection.ltr,
textScaleFactor: MediaQuery.textScaleFactorOf(context),
)..layout(const BoxConstraints(), parentUsesSize: true);
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
// from `RadioListTile` layout
const contentPadding = 32;
const leadingWidth = kMinInteractiveDimension + 8;
return contentPadding + leadingWidth + textWidth;
}
double tabBodyMaxHeight(BuildContext context) =>
[
sortOptions,
groupOptions,
layoutOptions,
].map((v) => v.length).fold(0, max) *
singleOptionTileHeight(context);
Widget _buildRadioListTile<T>(T value, String title, T? Function() get, void Function(T value) set) {
return RadioListTile<T>(
// key is expected by test driver
key: Key(value.toString()),
value: value,
groupValue: get(),
onChanged: (v) => setState(() => set(v!)),
title: Text(
title,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}
}

View file

@ -31,7 +31,6 @@ class _VideoSpeedDialogState extends State<VideoSpeedDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesDialog( return AvesDialog(
context: context,
horizontalContentPadding: 4, horizontalContentPadding: 4,
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View file

@ -47,7 +47,6 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
final canSelectText = _textStreams.length > 1; final canSelectText = _textStreams.length > 1;
final canSelect = canSelectVideo || canSelectAudio || canSelectText; final canSelect = canSelectVideo || canSelectAudio || canSelectText;
return AvesDialog( return AvesDialog(
context: context,
content: canSelect ? null : Text(context.l10n.videoStreamSelectionDialogNoSelection), content: canSelect ? null : Text(context.l10n.videoStreamSelectionDialogNoSelection),
scrollableContent: canSelect scrollableContent: canSelect
? [ ? [

View file

@ -1,3 +1,4 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
@ -24,6 +25,21 @@ import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
Future<String?> pickAlbum({
required BuildContext context,
required MoveType? moveType,
}) async {
final source = context.read<CollectionSource>();
final filter = await Navigator.push(
context,
MaterialPageRoute<AlbumFilter>(
settings: const RouteSettings(name: AlbumPickPage.routeName),
builder: (context) => AlbumPickPage(source: source, moveType: moveType),
),
);
return filter?.album;
}
class AlbumPickPage extends StatefulWidget { class AlbumPickPage extends StatefulWidget {
static const routeName = '/album_pick'; static const routeName = '/album_pick';
@ -47,45 +63,47 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<Settings, Tuple2<AlbumChipGroupFactor, ChipSortFactor>>( return ListenableProvider<ValueNotifier<AppMode>>.value(
selector: (context, s) => Tuple2(s.albumGroupFactor, s.albumSortFactor), value: ValueNotifier(AppMode.pickInternal),
builder: (context, s, child) { child: Selector<Settings, Tuple2<AlbumChipGroupFactor, ChipSortFactor>>(
return StreamBuilder( selector: (context, s) => Tuple2(s.albumGroupFactor, s.albumSortFactor),
stream: source.eventBus.on<AlbumsChangedEvent>(), builder: (context, s, child) {
builder: (context, snapshot) { return StreamBuilder(
final gridItems = AlbumListPage.getAlbumGridItems(context, source); stream: source.eventBus.on<AlbumsChangedEvent>(),
return SelectionProvider<FilterGridItem<AlbumFilter>>( builder: (context, snapshot) {
child: FilterGridPage<AlbumFilter>( final gridItems = AlbumListPage.getAlbumGridItems(context, source);
settingsRouteKey: AlbumListPage.routeName, return SelectionProvider<FilterGridItem<AlbumFilter>>(
appBar: AlbumPickAppBar( child: FilterGridPage<AlbumFilter>(
source: source, settingsRouteKey: AlbumListPage.routeName,
moveType: widget.moveType, appBar: AlbumPickAppBar(
actionDelegate: AlbumChipSetActionDelegate(gridItems), source: source,
moveType: widget.moveType,
actionDelegate: AlbumChipSetActionDelegate(gridItems),
queryNotifier: _queryNotifier,
),
appBarHeight: AlbumPickAppBar.preferredHeight,
sections: AlbumListPage.groupToSections(context, source, gridItems),
newFilters: source.getNewAlbumFilters(context),
sortFactor: settings.albumSortFactor,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
selectable: false,
queryNotifier: _queryNotifier, queryNotifier: _queryNotifier,
applyQuery: (filters, query) {
if (query.isEmpty) return filters;
query = query.toUpperCase();
return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
},
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: context.l10n.albumEmpty,
),
heroType: HeroType.never,
), ),
appBarHeight: AlbumPickAppBar.preferredHeight, );
sections: AlbumListPage.groupToSections(context, source, gridItems), },
newFilters: source.getNewAlbumFilters(context), );
sortFactor: settings.albumSortFactor, },
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, ),
selectable: false,
queryNotifier: _queryNotifier,
applyQuery: (filters, query) {
if (query.isEmpty) return filters;
query = query.toUpperCase();
return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
},
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: context.l10n.albumEmpty,
),
onTap: (filter) => Navigator.pop<String>(context, (filter as AlbumFilter).album),
heroType: HeroType.never,
),
);
},
);
},
); );
} }
} }
@ -152,12 +170,8 @@ class AlbumPickAppBar extends StatelessWidget {
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
PopupMenuItem( PopupMenuItem(
value: ChipSetAction.sort, value: ChipSetAction.configureView,
child: MenuRow(text: context.l10n.menuActionSort, icon: const Icon(AIcons.sort)), child: MenuRow(text: context.l10n.menuActionConfigureView, icon: const Icon(AIcons.view)),
),
PopupMenuItem(
value: ChipSetAction.group,
child: MenuRow(text: context.l10n.menuActionGroup, icon: const Icon(AIcons.group)),
), ),
]; ];
}, },

View file

@ -17,14 +17,16 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/rename_album_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/rename_album_dialog.dart';
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> { class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
final Iterable<FilterGridItem<AlbumFilter>> _items; final Iterable<FilterGridItem<AlbumFilter>> _items;
@ -40,6 +42,12 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
@override @override
set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor; set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor;
@override
TileLayout get tileLayout => settings.getTileLayout(AlbumListPage.routeName);
@override
set tileLayout(TileLayout tileLayout) => settings.setTileLayout(AlbumListPage.routeName, tileLayout);
@override @override
bool isVisible( bool isVisible(
ChipSetAction action, { ChipSetAction action, {
@ -49,8 +57,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
required Set<AlbumFilter> selectedFilters, required Set<AlbumFilter> selectedFilters,
}) { }) {
switch (action) { switch (action) {
case ChipSetAction.group:
return true;
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
return appMode == AppMode.main && !isSelecting; return appMode == AppMode.main && !isSelecting;
case ChipSetAction.delete: case ChipSetAction.delete:
@ -96,9 +102,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
void onActionSelected(BuildContext context, Set<AlbumFilter> filters, ChipSetAction action) { void onActionSelected(BuildContext context, Set<AlbumFilter> filters, ChipSetAction action) {
switch (action) { switch (action) {
// general // general
case ChipSetAction.group:
_group(context);
break;
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
_createAlbum(context); _createAlbum(context);
break; break;
@ -118,23 +121,42 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
void _browse(BuildContext context) => context.read<Selection<FilterGridItem<AlbumFilter>>>().browse(); void _browse(BuildContext context) => context.read<Selection<FilterGridItem<AlbumFilter>>>().browse();
Future<void> _group(BuildContext context) async { @override
final factor = await showDialog<AlbumChipGroupFactor>( Future<void> configureView(BuildContext context) async {
final initialValue = Tuple3(
sortFactor,
settings.albumGroupFactor,
tileLayout,
);
final value = await showDialog<Tuple3<ChipSortFactor?, AlbumChipGroupFactor?, TileLayout?>>(
context: context, context: context,
builder: (context) => AvesSelectionDialog<AlbumChipGroupFactor>( builder: (context) {
initialValue: settings.albumGroupFactor, final l10n = context.l10n;
options: { return TileViewDialog<ChipSortFactor, AlbumChipGroupFactor, TileLayout>(
AlbumChipGroupFactor.importance: context.l10n.albumGroupTier, initialValue: initialValue,
AlbumChipGroupFactor.volume: context.l10n.albumGroupVolume, sortOptions: {
AlbumChipGroupFactor.none: context.l10n.albumGroupNone, ChipSortFactor.date: context.l10n.chipSortDate,
}, ChipSortFactor.name: context.l10n.chipSortName,
title: context.l10n.albumGroupTitle, ChipSortFactor.count: context.l10n.chipSortCount,
), },
groupOptions: {
AlbumChipGroupFactor.importance: context.l10n.albumGroupTier,
AlbumChipGroupFactor.volume: context.l10n.albumGroupVolume,
AlbumChipGroupFactor.none: context.l10n.albumGroupNone,
},
layoutOptions: {
TileLayout.grid: l10n.tileLayoutGrid,
TileLayout.list: l10n.tileLayoutList,
},
);
},
); );
// wait for the dialog to hide as applying the change may block the UI // wait for the dialog to hide as applying the change may block the UI
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
if (factor != null) { if (value != null && initialValue != value) {
settings.albumGroupFactor = factor; sortFactor = value.item1!;
settings.albumGroupFactor = value.item2!;
tileLayout = value.item3!;
} }
} }
@ -172,7 +194,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)), content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)),
actions: [ actions: [
TextButton( TextButton(

View file

@ -35,7 +35,6 @@ class ChipActionDelegate {
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.hideFilterConfirmationDialogMessage), content: Text(context.l10n.hideFilterConfirmationDialogMessage),
actions: [ actions: [
TextButton( TextButton(

View file

@ -14,8 +14,8 @@ import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/cover_selection_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/cover_selection_dialog.dart';
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/stats/stats_page.dart'; import 'package:aves/widgets/stats/stats_page.dart';
@ -32,6 +32,10 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
set sortFactor(ChipSortFactor factor); set sortFactor(ChipSortFactor factor);
TileLayout get tileLayout;
set tileLayout(TileLayout tileLayout);
bool isVisible( bool isVisible(
ChipSetAction action, { ChipSetAction action, {
required AppMode appMode, required AppMode appMode,
@ -43,10 +47,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
final hasSelection = selectedFilters.isNotEmpty; final hasSelection = selectedFilters.isNotEmpty;
switch (action) { switch (action) {
// general // general
case ChipSetAction.sort: case ChipSetAction.configureView:
return true; return true;
case ChipSetAction.group:
return false;
case ChipSetAction.select: case ChipSetAction.select:
return appMode.canSelect && !isSelecting; return appMode.canSelect && !isSelecting;
case ChipSetAction.selectAll: case ChipSetAction.selectAll:
@ -91,8 +93,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
switch (action) { switch (action) {
// general // general
case ChipSetAction.sort: case ChipSetAction.configureView:
case ChipSetAction.group:
case ChipSetAction.select: case ChipSetAction.select:
case ChipSetAction.selectAll: case ChipSetAction.selectAll:
case ChipSetAction.selectNone: case ChipSetAction.selectNone:
@ -120,10 +121,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
void onActionSelected(BuildContext context, Set<T> filters, ChipSetAction action) { void onActionSelected(BuildContext context, Set<T> filters, ChipSetAction action) {
switch (action) { switch (action) {
// general // general
case ChipSetAction.sort: case ChipSetAction.configureView:
_showSortDialog(context); configureView(context);
break;
case ChipSetAction.group:
break; break;
case ChipSetAction.select: case ChipSetAction.select:
context.read<Selection<FilterGridItem<T>>>().select(); context.read<Selection<FilterGridItem<T>>>().select();
@ -178,23 +177,35 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
return filters.isEmpty ? visibleEntries : visibleEntries.where((entry) => filters.any((f) => f.test(entry))); return filters.isEmpty ? visibleEntries : visibleEntries.where((entry) => filters.any((f) => f.test(entry)));
} }
Future<void> _showSortDialog(BuildContext context) async { Future<void> configureView(BuildContext context) async {
final factor = await showDialog<ChipSortFactor>( final initialValue = Tuple3(
sortFactor,
null,
tileLayout,
);
final value = await showDialog<Tuple3<ChipSortFactor?, void, TileLayout?>>(
context: context, context: context,
builder: (context) => AvesSelectionDialog<ChipSortFactor>( builder: (context) {
initialValue: sortFactor, final l10n = context.l10n;
options: { return TileViewDialog<ChipSortFactor, void, TileLayout>(
ChipSortFactor.date: context.l10n.chipSortDate, initialValue: initialValue,
ChipSortFactor.name: context.l10n.chipSortName, sortOptions: {
ChipSortFactor.count: context.l10n.chipSortCount, ChipSortFactor.date: context.l10n.chipSortDate,
}, ChipSortFactor.name: context.l10n.chipSortName,
title: context.l10n.chipSortTitle, ChipSortFactor.count: context.l10n.chipSortCount,
), },
layoutOptions: {
TileLayout.grid: l10n.tileLayoutGrid,
TileLayout.list: l10n.tileLayoutList,
},
);
},
); );
// wait for the dialog to hide as applying the change may block the UI // wait for the dialog to hide as applying the change may block the UI
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
if (factor != null) { if (value != null && initialValue != value) {
sortFactor = factor; sortFactor = value.item1!;
tileLayout = value.item3!;
} }
} }
@ -244,7 +255,6 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.hideFilterConfirmationDialogMessage), content: Text(context.l10n.hideFilterConfirmationDialogMessage),
actions: [ actions: [
TextButton( TextButton(

View file

@ -3,6 +3,7 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> { class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> {
final Iterable<FilterGridItem<LocationFilter>> _items; final Iterable<FilterGridItem<LocationFilter>> _items;
@ -17,4 +18,10 @@ class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter>
@override @override
set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor; set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor;
@override
TileLayout get tileLayout => settings.getTileLayout(CountryListPage.routeName);
@override
set tileLayout(TileLayout tileLayout) => settings.setTileLayout(CountryListPage.routeName, tileLayout);
} }

View file

@ -3,6 +3,7 @@ import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> { class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
final Iterable<FilterGridItem<TagFilter>> _items; final Iterable<FilterGridItem<TagFilter>> _items;
@ -17,4 +18,10 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
@override @override
set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor; set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor;
@override
TileLayout get tileLayout => settings.getTileLayout(TagListPage.routeName);
@override
set tileLayout(TileLayout tileLayout) => settings.setTileLayout(TagListPage.routeName, tileLayout);
} }

View file

@ -12,11 +12,9 @@ import 'package:aves/model/source/tag.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -24,7 +22,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
final T filter; final T filter;
final double extent, thumbnailExtent; final double extent, thumbnailExtent;
final AvesEntry? coverEntry; final AvesEntry? coverEntry;
final bool pinned; final bool showText, pinned;
final String? banner; final String? banner;
final FilterCallback? onTap; final FilterCallback? onTap;
final HeroType heroType; final HeroType heroType;
@ -35,6 +33,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
required this.extent, required this.extent,
double? thumbnailExtent, double? thumbnailExtent,
this.coverEntry, this.coverEntry,
this.showText = true,
this.pinned = false, this.pinned = false,
this.banner, this.banner,
this.onTap, this.onTap,
@ -42,11 +41,14 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
}) : thumbnailExtent = thumbnailExtent ?? extent, }) : thumbnailExtent = thumbnailExtent ?? extent,
super(key: key); super(key: key);
static double tileHeight({required double extent, required double textScaleFactor}) { static double tileHeight({required double extent, required double textScaleFactor, required bool showText}) {
return extent + infoHeight(extent: extent, textScaleFactor: textScaleFactor); return extent + infoHeight(extent: extent, textScaleFactor: textScaleFactor, showText: showText);
} }
static double infoHeight({required double extent, required double textScaleFactor}) { // info includes title and content details
static double infoHeight({required double extent, required double textScaleFactor, required bool showText}) {
if (!showText) return 0;
// height can actually be a little larger or smaller, when info includes icons or non-latin scripts // height can actually be a little larger or smaller, when info includes icons or non-latin scripts
// but it's not worth measuring text metrics, as the widget is flexible enough to absorb the difference // but it's not worth measuring text metrics, as the widget is flexible enough to absorb the difference
return (AvesFilterChip.fontSize + detailFontSize(extent) + 4) * textScaleFactor + AvesFilterChip.decoratedContentVerticalPadding * 2; return (AvesFilterChip.fontSize + detailFontSize(extent) + 4) * textScaleFactor + AvesFilterChip.decoratedContentVerticalPadding * 2;
@ -106,13 +108,20 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
return AvesFilterChip( return AvesFilterChip(
key: chipKey, key: chipKey,
filter: filter, filter: filter,
showText: showText,
showGenericIcon: false, showGenericIcon: false,
decoration: AvesFilterDecoration( decoration: AvesFilterDecoration(
widget: Selector<MediaQueryData, double>( widget: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.textScaleFactor, selector: (context, mq) => mq.textScaleFactor,
builder: (context, textScaleFactor, child) { builder: (context, textScaleFactor, child) {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: infoHeight(extent: extent, textScaleFactor: textScaleFactor)), padding: EdgeInsets.only(
bottom: infoHeight(
extent: extent,
textScaleFactor: textScaleFactor,
showText: showText,
),
),
child: child, child: child,
); );
}, },
@ -142,7 +151,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
radius: radius(extent), radius: radius(extent),
), ),
banner: banner, banner: banner,
details: _buildDetails(source, filter), details: showText ? _buildDetails(source, filter) : null,
padding: titlePadding, padding: titlePadding,
heroType: heroType, heroType: heroType,
onTap: onTap, onTap: onTap,
@ -161,10 +170,9 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
AnimatedPadding( AnimatedPadding(
padding: EdgeInsets.only(right: padding), padding: EdgeInsets.only(right: padding),
duration: Durations.chipDecorationAnimation, duration: Durations.chipDecorationAnimation,
child: DecoratedIcon( child: Icon(
AIcons.pin, AIcons.pin,
color: FilterGridPage.detailColor, color: FilterGridPage.detailColor,
shadows: Constants.embossShadows,
size: iconSize, size: iconSize,
), ),
), ),
@ -172,10 +180,9 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
AnimatedPadding( AnimatedPadding(
padding: EdgeInsets.only(right: padding), padding: EdgeInsets.only(right: padding),
duration: Durations.chipDecorationAnimation, duration: Durations.chipDecorationAnimation,
child: DecoratedIcon( child: Icon(
AIcons.removableStorage, AIcons.removableStorage,
color: FilterGridPage.detailColor, color: FilterGridPage.detailColor,
shadows: Constants.embossShadows,
size: iconSize, size: iconSize,
), ),
), ),

View file

@ -7,12 +7,15 @@ import 'package:flutter/widgets.dart';
class FilterChipGridDecorator<T extends CollectionFilter, U extends FilterGridItem<T>> extends StatelessWidget { class FilterChipGridDecorator<T extends CollectionFilter, U extends FilterGridItem<T>> extends StatelessWidget {
final U gridItem; final U gridItem;
final double extent; final double extent;
final bool selectable, highlightable;
final Widget child; final Widget child;
const FilterChipGridDecorator({ const FilterChipGridDecorator({
Key? key, Key? key,
required this.gridItem, required this.gridItem,
required this.extent, required this.extent,
this.selectable = true,
this.highlightable = true,
required this.child, required this.child,
}) : super(key: key); }) : super(key: key);
@ -26,16 +29,18 @@ class FilterChipGridDecorator<T extends CollectionFilter, U extends FilterGridIt
fit: StackFit.passthrough, fit: StackFit.passthrough,
children: [ children: [
child, child,
GridItemSelectionOverlay<FilterGridItem<T>>( if (selectable)
item: gridItem, GridItemSelectionOverlay<FilterGridItem<T>>(
borderRadius: borderRadius, item: gridItem,
padding: EdgeInsets.all(extent / 24), borderRadius: borderRadius,
), padding: EdgeInsets.all(extent / 24),
ChipHighlightOverlay( ),
filter: gridItem.filter, if (highlightable)
extent: extent, ChipHighlightOverlay(
borderRadius: borderRadius, filter: gridItem.filter,
), extent: extent,
borderRadius: borderRadius,
),
], ],
), ),
); );

View file

@ -12,6 +12,7 @@ import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/item_tracker.dart'; import 'package:aves/widgets/common/grid/item_tracker.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:aves/widgets/common/grid/selector.dart'; import 'package:aves/widgets/common/grid/selector.dart';
import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/grid/sliver.dart';
@ -20,12 +21,12 @@ import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:aves/widgets/drawer/app_drawer.dart';
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart'; import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart';
import 'package:aves/widgets/filter_grids/common/filter_chip_grid_decorator.dart'; import 'package:aves/widgets/filter_grids/common/filter_tile.dart';
import 'package:aves/widgets/filter_grids/common/list_details_theme.dart';
import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart';
import 'package:aves/widgets/filter_grids/common/section_layout.dart'; import 'package:aves/widgets/filter_grids/common/section_layout.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -48,7 +49,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final ValueNotifier<String> queryNotifier; final ValueNotifier<String> queryNotifier;
final QueryTest<T>? applyQuery; final QueryTest<T>? applyQuery;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final FilterCallback onTap;
final HeroType heroType; final HeroType heroType;
const FilterGridPage({ const FilterGridPage({
@ -64,7 +64,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
required this.queryNotifier, required this.queryNotifier,
this.applyQuery, this.applyQuery,
required this.emptyBuilder, required this.emptyBuilder,
required this.onTap,
required this.heroType, required this.heroType,
}) : super(key: key); }) : super(key: key);
@ -103,7 +102,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
queryNotifier: queryNotifier, queryNotifier: queryNotifier,
applyQuery: applyQuery, applyQuery: applyQuery,
emptyBuilder: emptyBuilder, emptyBuilder: emptyBuilder,
onTap: onTap,
heroType: heroType, heroType: heroType,
), ),
), ),
@ -129,7 +127,6 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
final ValueNotifier<String> queryNotifier; final ValueNotifier<String> queryNotifier;
final QueryTest<T>? applyQuery; final QueryTest<T>? applyQuery;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final FilterCallback onTap;
final HeroType heroType; final HeroType heroType;
const FilterGrid({ const FilterGrid({
@ -145,7 +142,6 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
required this.queryNotifier, required this.queryNotifier,
required this.applyQuery, required this.applyQuery,
required this.emptyBuilder, required this.emptyBuilder,
required this.onTap,
required this.heroType, required this.heroType,
}) : super(key: key); }) : super(key: key);
@ -183,7 +179,6 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
queryNotifier: widget.queryNotifier, queryNotifier: widget.queryNotifier,
applyQuery: widget.applyQuery, applyQuery: widget.applyQuery,
emptyBuilder: widget.emptyBuilder, emptyBuilder: widget.emptyBuilder,
onTap: widget.onTap,
heroType: widget.heroType, heroType: widget.heroType,
), ),
); );
@ -199,7 +194,6 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
final ValueNotifier<String> queryNotifier; final ValueNotifier<String> queryNotifier;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final QueryTest<T>? applyQuery; final QueryTest<T>? applyQuery;
final FilterCallback onTap;
final HeroType heroType; final HeroType heroType;
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
@ -216,7 +210,6 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
required this.queryNotifier, required this.queryNotifier,
required this.applyQuery, required this.applyQuery,
required this.emptyBuilder, required this.emptyBuilder,
required this.onTap,
required this.heroType, required this.heroType,
}) : super(key: key) { }) : super(key: key) {
_appBarHeightNotifier.value = appBarHeight; _appBarHeightNotifier.value = appBarHeight;
@ -224,6 +217,8 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
return ValueListenableBuilder<String>( return ValueListenableBuilder<String>(
valueListenable: queryNotifier, valueListenable: queryNotifier,
builder: (context, query, child) { builder: (context, query, child) {
@ -240,10 +235,9 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
}); });
} }
final pinnedFilters = settings.pinnedFilters;
final sectionedListLayoutProvider = ValueListenableBuilder<double>( final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier), valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, tileExtent, child) { builder: (context, thumbnailExtent, child) {
return Selector<TileExtentController, Tuple3<double, int, double>>( return Selector<TileExtentController, Tuple3<double, int, double>>(
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
builder: (context, c, child) { builder: (context, c, child) {
@ -256,38 +250,37 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
return Selector<MediaQueryData, double>( return Selector<MediaQueryData, double>(
selector: (context, mq) => mq.textScaleFactor, selector: (context, mq) => mq.textScaleFactor,
builder: (context, textScaleFactor, child) { builder: (context, textScaleFactor, child) {
final tileHeight = CoveredFilterChip.tileHeight(extent: tileExtent, textScaleFactor: textScaleFactor); final tileHeight = CoveredFilterChip.tileHeight(
extent: thumbnailExtent,
textScaleFactor: textScaleFactor,
showText: tileLayout == TileLayout.grid,
);
return GridTheme( return GridTheme(
extent: tileExtent, extent: thumbnailExtent,
child: SectionedFilterListLayoutProvider<T>( child: FilterListDetailsTheme(
sections: visibleSections, extent: thumbnailExtent,
showHeaders: showHeaders, child: SectionedFilterListLayoutProvider<T>(
scrollableWidth: scrollableWidth, sections: visibleSections,
columnCount: columnCount, showHeaders: showHeaders,
spacing: tileSpacing, tileLayout: tileLayout,
tileWidth: tileExtent, scrollableWidth: scrollableWidth,
tileHeight: tileHeight, columnCount: columnCount,
tileBuilder: (gridItem) { spacing: tileSpacing,
final filter = gridItem.filter; tileWidth: thumbnailExtent,
return MetaData( tileHeight: tileHeight,
metaData: ScalerMetadata(gridItem), tileBuilder: (gridItem) {
child: FilterChipGridDecorator<T, FilterGridItem<T>>( return InteractiveFilterTile(
gridItem: gridItem, gridItem: gridItem,
extent: tileExtent, chipExtent: thumbnailExtent,
child: CoveredFilterChip( thumbnailExtent: thumbnailExtent,
key: Key(filter.key), tileLayout: tileLayout,
filter: filter, banner: _getFilterBanner(context, gridItem.filter),
extent: tileExtent, heroType: heroType,
pinned: pinnedFilters.contains(filter), );
banner: newFilters.contains(filter) ? context.l10n.newFilterBanner : null, },
onTap: onTap, tileAnimationDelay: tileAnimationDelay,
heroType: heroType, child: child!,
), ),
),
);
},
tileAnimationDelay: tileAnimationDelay,
child: child!,
), ),
); );
}, },
@ -304,13 +297,20 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
sortFactor: sortFactor, sortFactor: sortFactor,
selectable: selectable, selectable: selectable,
emptyBuilder: emptyBuilder, emptyBuilder: emptyBuilder,
bannerBuilder: _getFilterBanner,
scrollController: PrimaryScrollController.of(context)!, scrollController: PrimaryScrollController.of(context)!,
tileLayout: tileLayout,
), ),
); );
return sectionedListLayoutProvider; return sectionedListLayoutProvider;
}, },
); );
} }
String? _getFilterBanner(BuildContext context, T filter) {
final isNew = newFilters.contains(filter);
return isNew ? context.l10n.newFilterBanner : null;
}
} }
class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget { class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget {
@ -320,7 +320,9 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
final ChipSortFactor sortFactor; final ChipSortFactor sortFactor;
final bool selectable; final bool selectable;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final String? Function(BuildContext context, T filter) bannerBuilder;
final ScrollController scrollController; final ScrollController scrollController;
final TileLayout tileLayout;
const _FilterSectionedContent({ const _FilterSectionedContent({
required this.appBar, required this.appBar,
@ -329,27 +331,28 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
required this.sortFactor, required this.sortFactor,
required this.selectable, required this.selectable,
required this.emptyBuilder, required this.emptyBuilder,
required this.bannerBuilder,
required this.scrollController, required this.scrollController,
required this.tileLayout,
}); });
@override @override
_FilterSectionedContentState createState() => _FilterSectionedContentState<T>(); _FilterSectionedContentState createState() => _FilterSectionedContentState<T>();
} }
class _FilterSectionedContentState<T extends CollectionFilter> extends State<_FilterSectionedContent<T>> with WidgetsBindingObserver, GridItemTrackerMixin<FilterGridItem<T>, _FilterSectionedContent<T>> { class _FilterSectionedContentState<T extends CollectionFilter> extends State<_FilterSectionedContent<T>> {
Widget get appBar => widget.appBar; Widget get appBar => widget.appBar;
@override
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier; ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
TileLayout get tileLayout => widget.tileLayout;
Map<ChipSectionKey, List<FilterGridItem<T>>> get visibleSections => widget.visibleSections; Map<ChipSectionKey, List<FilterGridItem<T>>> get visibleSections => widget.visibleSections;
Widget Function() get emptyBuilder => widget.emptyBuilder; Widget Function() get emptyBuilder => widget.emptyBuilder;
@override
ScrollController get scrollController => widget.scrollController; ScrollController get scrollController => widget.scrollController;
@override
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable'); final GlobalKey scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
@override @override
@ -374,6 +377,8 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
final scaler = _FilterScaler<T>( final scaler = _FilterScaler<T>(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
appBarHeightNotifier: appBarHeightNotifier, appBarHeightNotifier: appBarHeightNotifier,
tileLayout: tileLayout,
bannerBuilder: widget.bannerBuilder,
child: scrollView, child: scrollView,
); );
@ -386,7 +391,13 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
child: scaler, child: scaler,
); );
return selector; return GridItemTracker<FilterGridItem<T>>(
scrollableKey: scrollableKey,
tileLayout: tileLayout,
appBarHeightNotifier: appBarHeightNotifier,
scrollController: scrollController,
child: selector,
);
} }
Future<void> _checkInitHighlight() async { Future<void> _checkInitHighlight() async {
@ -405,43 +416,48 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
class _FilterScaler<T extends CollectionFilter> extends StatelessWidget { class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
final GlobalKey scrollableKey; final GlobalKey scrollableKey;
final ValueNotifier<double> appBarHeightNotifier; final ValueNotifier<double> appBarHeightNotifier;
final TileLayout tileLayout;
final String? Function(BuildContext context, T filter) bannerBuilder;
final Widget child; final Widget child;
const _FilterScaler({ const _FilterScaler({
required this.scrollableKey, required this.scrollableKey,
required this.appBarHeightNotifier, required this.appBarHeightNotifier,
required this.tileLayout,
required this.bannerBuilder,
required this.child, required this.child,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pinnedFilters = settings.pinnedFilters;
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing); final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor); final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor);
return GridScaleGestureDetector<FilterGridItem<T>>( return GridScaleGestureDetector<FilterGridItem<T>>(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
heightForWidth: (width) => CoveredFilterChip.tileHeight(extent: width, textScaleFactor: textScaleFactor), tileLayout: tileLayout,
heightForWidth: (width) => CoveredFilterChip.tileHeight(extent: width, textScaleFactor: textScaleFactor, showText: true),
gridBuilder: (center, tileSize, child) => CustomPaint( gridBuilder: (center, tileSize, child) => CustomPaint(
painter: GridPainter( painter: GridPainter(
center: center, tileLayout: tileLayout,
tileCenter: center,
tileSize: tileSize, tileSize: tileSize,
spacing: tileSpacing, spacing: tileSpacing,
borderWidth: AvesFilterChip.outlineWidth, borderWidth: AvesFilterChip.outlineWidth,
borderRadius: CoveredFilterChip.radius(tileSize.width), borderRadius: CoveredFilterChip.radius(tileSize.shortestSide),
color: Colors.grey.shade700, color: Colors.grey.shade700,
), ),
child: child, child: child,
), ),
scaledBuilder: (item, tileSize) { scaledBuilder: (item, tileSize) => FilterListDetailsTheme(
final filter = item.filter; extent: tileSize.height,
return CoveredFilterChip( child: FilterTile(
filter: filter, gridItem: item,
extent: tileSize.width, chipExtent: tileLayout == TileLayout.grid ? tileSize.width : tileSize.height,
thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax, thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
pinned: pinnedFilters.contains(filter), tileLayout: tileLayout,
heroType: HeroType.never, banner: bannerBuilder(context, item.filter),
); ),
}, ),
highlightItem: (item) => item.filter, highlightItem: (item) => item.filter,
child: child, child: child,
); );

View file

@ -1,8 +1,6 @@
import 'package:aves/model/filters/filters.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/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
@ -56,28 +54,14 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox.shrink(); return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox.shrink();
}, },
), ),
onTap: (filter) => _goToCollection(context, filter), // do not always enable hero, otherwise unwanted hero gets triggered
// when using `Show in [...]` action from a chip in the Collection filter bar
heroType: HeroType.onTap, heroType: HeroType.onTap,
), ),
), ),
); );
} }
void _goToCollection(BuildContext context, CollectionFilter filter) {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage(
collection: CollectionLens(
source: source,
filters: {filter},
),
),
),
);
}
static int compareFiltersByDate(FilterGridItem<CollectionFilter> a, FilterGridItem<CollectionFilter> b) { static int compareFiltersByDate(FilterGridItem<CollectionFilter> a, FilterGridItem<CollectionFilter> b) {
final c = (b.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0)).compareTo(a.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0)); final c = (b.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0)).compareTo(a.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0));
return c != 0 ? c : a.filter.compareTo(b.filter); return c != 0 ? c : a.filter.compareTo(b.filter);

View file

@ -0,0 +1,190 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
import 'package:aves/widgets/filter_grids/common/filter_chip_grid_decorator.dart';
import 'package:aves/widgets/filter_grids/common/list_details.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class InteractiveFilterTile<T extends CollectionFilter> extends StatefulWidget {
final FilterGridItem<T> gridItem;
final double chipExtent, thumbnailExtent;
final TileLayout tileLayout;
final String? banner;
final HeroType heroType;
const InteractiveFilterTile({
Key? key,
required this.gridItem,
required this.chipExtent,
required this.thumbnailExtent,
required this.tileLayout,
this.banner,
required this.heroType,
}) : super(key: key);
@override
_InteractiveFilterTileState createState() => _InteractiveFilterTileState<T>();
}
class _InteractiveFilterTileState<T extends CollectionFilter> extends State<InteractiveFilterTile<T>> {
HeroType? _heroTypeOverride;
FilterGridItem<T> get gridItem => widget.gridItem;
HeroType get effectiveHeroType => _heroTypeOverride ?? widget.heroType;
@override
Widget build(BuildContext context) {
final filter = gridItem.filter;
void onTap() {
final appMode = context.read<ValueNotifier<AppMode>>().value;
switch (appMode) {
case AppMode.main:
final selection = context.read<Selection<FilterGridItem<T>>>();
if (selection.isSelecting) {
selection.toggleSelection(gridItem);
} else {
_goToCollection(context, filter);
}
break;
case AppMode.pickInternal:
Navigator.pop<T>(context, filter);
break;
case AppMode.pickExternal:
case AppMode.view:
break;
}
}
return MetaData(
metaData: ScalerMetadata(gridItem),
child: FilterTile(
gridItem: gridItem,
chipExtent: widget.chipExtent,
thumbnailExtent: widget.thumbnailExtent,
tileLayout: widget.tileLayout,
banner: widget.banner,
selectable: true,
highlightable: true,
onTap: onTap,
heroType: effectiveHeroType,
),
);
}
void _goToCollection(BuildContext context, CollectionFilter filter) {
if (effectiveHeroType == HeroType.onTap) {
// make sure the chip hero triggers, even when tapping on the list view details
setState(() => _heroTypeOverride = HeroType.always);
}
WidgetsBinding.instance!.addPostFrameCallback((_) {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage(
collection: CollectionLens(
source: context.read<CollectionSource>(),
filters: {filter},
),
),
),
);
});
}
}
class FilterTile<T extends CollectionFilter> extends StatelessWidget {
final FilterGridItem<T> gridItem;
final double chipExtent, thumbnailExtent;
final TileLayout tileLayout;
final String? banner;
final bool selectable, highlightable;
final VoidCallback? onTap;
final HeroType heroType;
const FilterTile({
Key? key,
required this.gridItem,
required this.chipExtent,
required this.thumbnailExtent,
required this.tileLayout,
this.banner,
this.selectable = false,
this.highlightable = false,
this.onTap,
this.heroType = HeroType.never,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final filter = gridItem.filter;
final pinned = settings.pinnedFilters.contains(filter);
final onChipTap = onTap != null ? (filter) => onTap?.call() : null;
switch (tileLayout) {
case TileLayout.grid:
return FilterChipGridDecorator<T, FilterGridItem<T>>(
gridItem: gridItem,
extent: chipExtent,
selectable: selectable,
highlightable: highlightable,
child: CoveredFilterChip(
filter: filter,
extent: chipExtent,
thumbnailExtent: thumbnailExtent,
showText: true,
pinned: pinned,
banner: banner,
onTap: onChipTap,
heroType: heroType,
),
);
case TileLayout.list:
Widget child = Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilterChipGridDecorator<T, FilterGridItem<T>>(
gridItem: gridItem,
extent: chipExtent,
selectable: selectable,
highlightable: highlightable,
child: CoveredFilterChip(
filter: filter,
extent: chipExtent,
thumbnailExtent: thumbnailExtent,
showText: false,
banner: banner,
onTap: onChipTap,
heroType: heroType,
),
),
Expanded(
child: FilterListDetails(
gridItem: gridItem,
pinned: pinned,
),
),
],
);
if (onTap != null) {
child = InkWell(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(123), bottomLeft: Radius.circular(123)),
onTap: onTap,
child: child,
);
}
return child;
}
}
}

View file

@ -0,0 +1,160 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/filter_grids/common/list_details_theme.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FilterListDetails<T extends CollectionFilter> extends StatelessWidget {
final FilterGridItem<T> gridItem;
final bool pinned;
T get filter => gridItem.filter;
AvesEntry? get entry => gridItem.entry;
const FilterListDetails({
Key? key,
required this.gridItem,
required this.pinned,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final detailsTheme = context.watch<FilterListDetailsThemeData>();
final leading = filter.iconBuilder(context, detailsTheme.titleIconSize, showGenericIcon: false);
final hasTitleLeading = leading != null;
return Container(
padding: FilterListDetailsTheme.contentPadding,
foregroundDecoration: BoxDecoration(
border: Border(top: AvesBorder.side),
),
margin: FilterListDetailsTheme.contentMargin,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
if (hasTitleLeading)
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: const EdgeInsets.only(right: FilterListDetailsTheme.titleIconPadding),
child: IconTheme(
data: IconThemeData(color: detailsTheme.titleStyle.color),
child: leading!,
),
),
),
TextSpan(
text: filter.getLabel(context),
style: detailsTheme.titleStyle,
),
],
),
softWrap: false,
overflow: detailsTheme.titleMaxLines == 1 ? TextOverflow.fade : TextOverflow.ellipsis,
maxLines: detailsTheme.titleMaxLines,
// `textScaleFactor` is applied to font size and icon size at the theme level,
// otherwise the leading icon will be low-res scaled up/down
textScaleFactor: 1,
),
const SizedBox(height: FilterListDetailsTheme.titleDetailPadding),
if (detailsTheme.showDate) _buildDateRow(context, detailsTheme, hasTitleLeading),
if (detailsTheme.showCount) _buildCountRow(context, detailsTheme, hasTitleLeading),
],
),
);
}
Widget _buildDateRow(BuildContext context, FilterListDetailsThemeData detailsTheme, bool hasTitleLeading) {
final locale = context.l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final date = entry?.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown;
Widget leading = const Icon(AIcons.date);
if (hasTitleLeading) {
leading = ConstrainedBox(
constraints: BoxConstraints(minWidth: detailsTheme.titleIconSize),
child: leading,
);
}
return IconTheme.merge(
data: detailsTheme.captionIconTheme,
child: Row(
children: [
leading,
const SizedBox(width: 8),
Expanded(
child: Text(
dateText,
style: detailsTheme.captionStyle,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
],
),
);
}
Widget _buildCountRow(BuildContext context, FilterListDetailsThemeData detailsTheme, bool hasTitleLeading) {
final _filter = filter;
final removableStorage = _filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(_filter.album);
List<Widget> leadingIcons = [
if (pinned) const Icon(AIcons.pin),
if (removableStorage) const Icon(AIcons.removableStorage),
];
Widget? leading;
if (leadingIcons.isNotEmpty) {
leading = Row(
children: leadingIcons
.mapIndexed((i, child) => i > 0
? Padding(
padding: const EdgeInsets.only(left: 8),
child: child,
)
: child)
.toList(),
);
}
leading = ConstrainedBox(
constraints: BoxConstraints(minWidth: hasTitleLeading ? detailsTheme.titleIconSize : detailsTheme.captionIconTheme.size!),
child: Center(child: leading ?? const SizedBox()),
);
return IconTheme.merge(
data: detailsTheme.captionIconTheme,
child: Row(
children: [
leading,
const SizedBox(width: 8),
Text(
context.l10n.itemCount(context.read<CollectionSource>().count(filter)),
style: detailsTheme.captionStyle,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
],
),
);
}
}

View file

@ -0,0 +1,109 @@
import 'dart:math';
import 'package:aves/theme/format.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
class FilterListDetailsTheme extends StatelessWidget {
final double extent;
final Widget child;
static const EdgeInsets contentMargin = EdgeInsets.symmetric(horizontal: 8);
static const EdgeInsets contentPadding = EdgeInsets.symmetric(vertical: 4);
static const double titleIconPadding = 8;
static const double titleDetailPadding = 6;
const FilterListDetailsTheme({
Key? key,
required this.extent,
required this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ProxyProvider<MediaQueryData, FilterListDetailsThemeData>(
update: (context, mq, previous) {
final locale = context.l10n.localeName;
final use24hour = mq.alwaysUse24HourFormat;
final textScaleFactor = mq.textScaleFactor;
final textTheme = Theme.of(context).textTheme;
final titleStyleBase = textTheme.bodyText2!;
final titleStyle = titleStyleBase.copyWith(fontSize: titleStyleBase.fontSize! * textScaleFactor);
final captionStyle = textTheme.caption!;
final titleIconSize = AvesFilterChip.iconSize * textScaleFactor;
final titleLineHeight = (RenderParagraph(
TextSpan(text: 'Fake Title', style: titleStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
final captionLineHeight = (RenderParagraph(
TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
strutStyle: Constants.overflowStrutStyle,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
var titleMaxLines = 1;
var showCount = false;
var showDate = false;
var availableHeight = extent - contentMargin.vertical - contentPadding.vertical;
final firstTitleLineHeight = max(titleLineHeight, titleIconSize);
if (availableHeight >= firstTitleLineHeight + titleDetailPadding + captionLineHeight) {
showCount = true;
availableHeight -= firstTitleLineHeight + titleDetailPadding + captionLineHeight;
if (availableHeight >= captionLineHeight) {
showDate = true;
availableHeight -= captionLineHeight;
titleMaxLines += availableHeight ~/ titleLineHeight;
}
}
return FilterListDetailsThemeData(
extent: extent,
titleMaxLines: titleMaxLines,
showCount: showCount,
showDate: showDate,
titleStyle: titleStyle,
captionStyle: captionStyle,
titleIconSize: titleIconSize,
captionIconTheme: IconThemeData(
color: captionStyle.color,
size: captionStyle.fontSize! * textScaleFactor,
),
);
},
child: child,
);
}
}
class FilterListDetailsThemeData {
final double extent;
final int titleMaxLines;
final bool showCount, showDate;
final TextStyle titleStyle, captionStyle;
final double titleIconSize;
final IconThemeData captionIconTheme;
const FilterListDetailsThemeData({
required this.extent,
required this.titleMaxLines,
required this.showCount,
required this.showDate,
required this.titleStyle,
required this.captionStyle,
required this.titleIconSize,
required this.captionIconTheme,
});
}

View file

@ -1,4 +1,5 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:aves/widgets/filter_grids/common/section_header.dart'; import 'package:aves/widgets/filter_grids/common/section_header.dart';
@ -11,6 +12,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
required this.sections, required this.sections,
required this.showHeaders, required this.showHeaders,
required double scrollableWidth, required double scrollableWidth,
required TileLayout tileLayout,
required int columnCount, required int columnCount,
required double spacing, required double spacing,
required double tileWidth, required double tileWidth,
@ -21,6 +23,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
}) : super( }) : super(
key: key, key: key,
scrollableWidth: scrollableWidth, scrollableWidth: scrollableWidth,
tileLayout: tileLayout,
columnCount: columnCount, columnCount: columnCount,
spacing: spacing, spacing: spacing,
tileWidth: tileWidth, tileWidth: tileWidth,

View file

@ -104,7 +104,7 @@ class _AddressRowState extends State<_AddressRow> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(width: MapInfoRow.iconPadding), const SizedBox(width: MapInfoRow.iconPadding),
const DecoratedIcon(AIcons.location, shadows: Constants.embossShadows, size: MapInfoRow.iconSize), const DecoratedIcon(AIcons.location, size: MapInfoRow.iconSize),
const SizedBox(width: MapInfoRow.iconPadding), const SizedBox(width: MapInfoRow.iconPadding),
Expanded( Expanded(
child: Container( child: Container(
@ -175,7 +175,7 @@ class _DateRow extends StatelessWidget {
return Row( return Row(
children: [ children: [
const SizedBox(width: MapInfoRow.iconPadding), const SizedBox(width: MapInfoRow.iconPadding),
const DecoratedIcon(AIcons.date, shadows: Constants.embossShadows, size: MapInfoRow.iconSize), const DecoratedIcon(AIcons.date, size: MapInfoRow.iconSize),
const SizedBox(width: MapInfoRow.iconPadding), const SizedBox(width: MapInfoRow.iconPadding),
Text( Text(
dateText, dateText,

View file

@ -63,20 +63,9 @@ class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
icon: const Icon(AIcons.add), icon: const Icon(AIcons.add),
label: context.l10n.settingsNavigationDrawerAddAlbum, label: context.l10n.settingsNavigationDrawerAddAlbum,
onPressed: () async { onPressed: () async {
final source = context.read<CollectionSource>(); final album = await pickAlbum(context: context, moveType: null);
final album = await Navigator.push( if (album == null) return;
context, setState(() => widget.items.add(album));
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);
});
}, },
) )
], ],

View file

@ -161,7 +161,6 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(1)), content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(1)),
actions: [ actions: [
TextButton( TextButton(
@ -197,15 +196,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
await source.init(); await source.init();
unawaited(source.refresh()); unawaited(source.refresh());
} }
final destinationAlbum = await Navigator.push( final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
context, if (destinationAlbum == null) return;
MaterialPageRoute<String>(
settings: const RouteSettings(name: AlbumPickPage.routeName),
builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export),
),
);
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return; if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return;

View file

@ -62,7 +62,6 @@ abstract class AvesVideoController {
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))),
actions: [ actions: [
TextButton( TextButton(

View file

@ -1 +1,8 @@
{} {
"ru": [
"menuActionConfigureView",
"viewDialogTabLayout",
"tileLayoutGrid",
"tileLayoutList"
]
}