#78 list view
This commit is contained in:
parent
07183de5fb
commit
51ff287dcd
68 changed files with 1781 additions and 626 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "묶음 없음",
|
||||||
|
|
|
@ -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": "Не группировать",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 }
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
95
lib/widgets/collection/grid/list_details.dart
Normal file
95
lib/widgets/collection/grid/list_details.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
99
lib/widgets/collection/grid/list_details_theme.dart
Normal file
99
lib/widgets/collection/grid/list_details_theme.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
287
lib/widgets/dialogs/tile_view_dialog.dart
Normal file
287
lib/widgets/dialogs/tile_view_dialog.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
? [
|
? [
|
||||||
|
|
|
@ -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)),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
190
lib/widgets/filter_grids/common/filter_tile.dart
Normal file
190
lib/widgets/filter_grids/common/filter_tile.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
lib/widgets/filter_grids/common/list_details.dart
Normal file
160
lib/widgets/filter_grids/common/list_details.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
109
lib/widgets/filter_grids/common/list_details_theme.dart
Normal file
109
lib/widgets/filter_grids/common/list_details_theme.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -1 +1,8 @@
|
||||||
{}
|
{
|
||||||
|
"ru": [
|
||||||
|
"menuActionConfigureView",
|
||||||
|
"viewDialogTabLayout",
|
||||||
|
"tileLayoutGrid",
|
||||||
|
"tileLayoutList"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue