#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": {},
|
||||
|
||||
"menuActionSort": "Sort",
|
||||
"@menuActionSort": {},
|
||||
"menuActionGroup": "Group",
|
||||
"@menuActionGroup": {},
|
||||
"menuActionConfigureView": "View",
|
||||
"@menuActionConfigureView": {},
|
||||
"menuActionSelect": "Select",
|
||||
"@menuActionSelect": {},
|
||||
"menuActionSelectAll": "Select all",
|
||||
|
@ -462,6 +460,18 @@
|
|||
"menuActionStats": "Stats",
|
||||
"@menuActionStats": {},
|
||||
|
||||
"viewDialogTabSort": "Sort",
|
||||
"@viewDialogTabSort": {},
|
||||
"viewDialogTabGroup": "Group",
|
||||
"@viewDialogTabGroup": {},
|
||||
"viewDialogTabLayout": "Layout",
|
||||
"@viewDialogTabLayout": {},
|
||||
|
||||
"tileLayoutGrid": "Grid",
|
||||
"@tileLayoutGrid": {},
|
||||
"tileLayoutList": "List",
|
||||
"@tileLayoutList": {},
|
||||
|
||||
"aboutPageTitle": "About",
|
||||
"@aboutPageTitle": {},
|
||||
"aboutLinkSources": "Sources",
|
||||
|
@ -566,8 +576,6 @@
|
|||
"collectionSearchTitlesHintText": "Search titles",
|
||||
"@collectionSearchTitlesHintText": {},
|
||||
|
||||
"collectionSortTitle": "Sort",
|
||||
"@collectionSortTitle": {},
|
||||
"collectionSortDate": "By date",
|
||||
"@collectionSortDate": {},
|
||||
"collectionSortSize": "By size",
|
||||
|
@ -575,8 +583,6 @@
|
|||
"collectionSortName": "By album & file name",
|
||||
"@collectionSortName": {},
|
||||
|
||||
"collectionGroupTitle": "Group",
|
||||
"@collectionGroupTitle": {},
|
||||
"collectionGroupAlbum": "By album",
|
||||
"@collectionGroupAlbum": {},
|
||||
"collectionGroupMonth": "By month",
|
||||
|
@ -674,8 +680,6 @@
|
|||
"drawerCollectionSphericalVideos": "360° Videos",
|
||||
"@drawerCollectionSphericalVideos": {},
|
||||
|
||||
"chipSortTitle": "Sort",
|
||||
"@chipSortTitle": {},
|
||||
"chipSortDate": "By date",
|
||||
"@chipSortDate": {},
|
||||
"chipSortName": "By name",
|
||||
|
@ -683,8 +687,6 @@
|
|||
"chipSortCount": "By item count",
|
||||
"@chipSortCount": {},
|
||||
|
||||
"albumGroupTitle": "Group",
|
||||
"@albumGroupTitle": {},
|
||||
"albumGroupTier": "By tier",
|
||||
"@albumGroupTier": {},
|
||||
"albumGroupVolume": "By storage volume",
|
||||
|
|
|
@ -203,14 +203,20 @@
|
|||
"genericSuccessFeedback": "Succès !",
|
||||
"genericFailureFeedback": "Échec",
|
||||
|
||||
"menuActionSort": "Trier",
|
||||
"menuActionGroup": "Grouper",
|
||||
"menuActionConfigureView": "Vue",
|
||||
"menuActionSelect": "Sélectionner",
|
||||
"menuActionSelectAll": "Tout sélectionner",
|
||||
"menuActionSelectNone": "Tout désélectionner",
|
||||
"menuActionMap": "Carte",
|
||||
"menuActionStats": "Statistiques",
|
||||
|
||||
"viewDialogTabSort": "Tri",
|
||||
"viewDialogTabGroup": "Groupes",
|
||||
"viewDialogTabLayout": "Vue",
|
||||
|
||||
"tileLayoutGrid": "Grille",
|
||||
"tileLayoutList": "Liste",
|
||||
|
||||
"aboutPageTitle": "À propos",
|
||||
"aboutLinkSources": "Sources",
|
||||
"aboutLinkLicense": "Licence",
|
||||
|
@ -261,12 +267,10 @@
|
|||
|
||||
"collectionSearchTitlesHintText": "Recherche de titres",
|
||||
|
||||
"collectionSortTitle": "Trier",
|
||||
"collectionSortDate": "par date",
|
||||
"collectionSortSize": "par taille",
|
||||
"collectionSortName": "alphabétiquement",
|
||||
"collectionSortName": "alphabétique",
|
||||
|
||||
"collectionGroupTitle": "Grouper",
|
||||
"collectionGroupAlbum": "par album",
|
||||
"collectionGroupMonth": "par mois",
|
||||
"collectionGroupDay": "par jour",
|
||||
|
@ -302,12 +306,10 @@
|
|||
"drawerCollectionRaws": "Photos Raw",
|
||||
"drawerCollectionSphericalVideos": "Vidéos à 360°",
|
||||
|
||||
"chipSortTitle": "Trier",
|
||||
"chipSortDate": "par date",
|
||||
"chipSortName": "par nom",
|
||||
"chipSortName": "alphabétique",
|
||||
"chipSortCount": "par nombre d’éléments",
|
||||
|
||||
"albumGroupTitle": "Grouper",
|
||||
"albumGroupTier": "par importance",
|
||||
"albumGroupVolume": "par volume de stockage",
|
||||
"albumGroupNone": "ne pas grouper",
|
||||
|
|
|
@ -203,14 +203,20 @@
|
|||
"genericSuccessFeedback": "정상 처리됐습니다",
|
||||
"genericFailureFeedback": "오류가 발생했습니다",
|
||||
|
||||
"menuActionSort": "정렬",
|
||||
"menuActionGroup": "묶음",
|
||||
"menuActionConfigureView": "뷰 설정",
|
||||
"menuActionSelect": "선택",
|
||||
"menuActionSelectAll": "모두 선택",
|
||||
"menuActionSelectNone": "모두 해제",
|
||||
"menuActionMap": "지도",
|
||||
"menuActionStats": "통계",
|
||||
|
||||
"viewDialogTabSort": "정렬",
|
||||
"viewDialogTabGroup": "묶음",
|
||||
"viewDialogTabLayout": "배치",
|
||||
|
||||
"tileLayoutGrid": "그리드",
|
||||
"tileLayoutList": "목록",
|
||||
|
||||
"aboutPageTitle": "앱 정보",
|
||||
"aboutLinkSources": "소스 코드",
|
||||
"aboutLinkLicense": "라이선스",
|
||||
|
@ -261,12 +267,10 @@
|
|||
|
||||
"collectionSearchTitlesHintText": "제목 검색",
|
||||
|
||||
"collectionSortTitle": "정렬",
|
||||
"collectionSortDate": "날짜",
|
||||
"collectionSortSize": "크기",
|
||||
"collectionSortName": "이름",
|
||||
|
||||
"collectionGroupTitle": "묶음",
|
||||
"collectionGroupAlbum": "앨범별로",
|
||||
"collectionGroupMonth": "월별로",
|
||||
"collectionGroupDay": "날짜별로",
|
||||
|
@ -302,12 +306,10 @@
|
|||
"drawerCollectionRaws": "Raw 이미지",
|
||||
"drawerCollectionSphericalVideos": "360° 동영상",
|
||||
|
||||
"chipSortTitle": "정렬",
|
||||
"chipSortDate": "날짜",
|
||||
"chipSortName": "이름",
|
||||
"chipSortCount": "항목수",
|
||||
|
||||
"albumGroupTitle": "묶음",
|
||||
"albumGroupTier": "단계별로",
|
||||
"albumGroupVolume": "저장공간별로",
|
||||
"albumGroupNone": "묶음 없음",
|
||||
|
|
|
@ -203,14 +203,15 @@
|
|||
"genericSuccessFeedback": "Выполнено!",
|
||||
"genericFailureFeedback": "Не удалось",
|
||||
|
||||
"menuActionSort": "Сортировка",
|
||||
"menuActionGroup": "Группировка",
|
||||
"menuActionSelect": "Выбрать",
|
||||
"menuActionSelectAll": "Выбрать все",
|
||||
"menuActionSelectNone": "Снять выделение",
|
||||
"menuActionMap": "Карта",
|
||||
"menuActionStats": "Статистика",
|
||||
|
||||
"viewDialogTabSort": "Сортировка",
|
||||
"viewDialogTabGroup": "Группировка",
|
||||
|
||||
"aboutPageTitle": "О нас",
|
||||
"aboutLinkSources": "Исходники",
|
||||
"aboutLinkLicense": "Лицензия",
|
||||
|
@ -261,12 +262,10 @@
|
|||
|
||||
"collectionSearchTitlesHintText": "Поиск заголовков",
|
||||
|
||||
"collectionSortTitle": "Сортировка",
|
||||
"collectionSortDate": "По дате",
|
||||
"collectionSortSize": "По размеру",
|
||||
"collectionSortName": "По имени альбома и файла",
|
||||
|
||||
"collectionGroupTitle": "Группировка",
|
||||
"collectionGroupAlbum": "По альбому",
|
||||
"collectionGroupMonth": "По месяцу",
|
||||
"collectionGroupDay": "По дню",
|
||||
|
@ -302,12 +301,10 @@
|
|||
"drawerCollectionRaws": "RAW",
|
||||
"drawerCollectionSphericalVideos": "360° видео",
|
||||
|
||||
"chipSortTitle": "Сортировка",
|
||||
"chipSortDate": "По дате",
|
||||
"chipSortName": "По названию",
|
||||
"chipSortCount": "По количеству объектов",
|
||||
|
||||
"albumGroupTitle": "Группировка",
|
||||
"albumGroupTier": "По уровню",
|
||||
"albumGroupVolume": "По накопителю",
|
||||
"albumGroupNone": "Не группировать",
|
||||
|
|
|
@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
enum ChipSetAction {
|
||||
// general
|
||||
sort,
|
||||
group,
|
||||
configureView,
|
||||
select,
|
||||
selectAll,
|
||||
selectNone,
|
||||
|
@ -27,8 +26,7 @@ enum ChipSetAction {
|
|||
|
||||
class ChipSetActions {
|
||||
static const general = [
|
||||
ChipSetAction.sort,
|
||||
ChipSetAction.group,
|
||||
ChipSetAction.configureView,
|
||||
ChipSetAction.select,
|
||||
ChipSetAction.selectAll,
|
||||
ChipSetAction.selectNone,
|
||||
|
@ -57,10 +55,8 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
return context.l10n.menuActionSort;
|
||||
case ChipSetAction.group:
|
||||
return context.l10n.menuActionGroup;
|
||||
case ChipSetAction.configureView:
|
||||
return context.l10n.menuActionConfigureView;
|
||||
case ChipSetAction.select:
|
||||
return context.l10n.menuActionSelect;
|
||||
case ChipSetAction.selectAll:
|
||||
|
@ -101,10 +97,8 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
IconData _getIconData() {
|
||||
switch (this) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
return AIcons.sort;
|
||||
case ChipSetAction.group:
|
||||
return AIcons.group;
|
||||
case ChipSetAction.configureView:
|
||||
return AIcons.view;
|
||||
case ChipSetAction.select:
|
||||
return AIcons.select;
|
||||
case ChipSetAction.selectAll:
|
||||
|
|
|
@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
enum EntrySetAction {
|
||||
// general
|
||||
sort,
|
||||
group,
|
||||
configureView,
|
||||
select,
|
||||
selectAll,
|
||||
selectNone,
|
||||
|
@ -32,8 +31,7 @@ enum EntrySetAction {
|
|||
|
||||
class EntrySetActions {
|
||||
static const general = [
|
||||
EntrySetAction.sort,
|
||||
EntrySetAction.group,
|
||||
EntrySetAction.configureView,
|
||||
EntrySetAction.select,
|
||||
EntrySetAction.selectAll,
|
||||
EntrySetAction.selectNone,
|
||||
|
@ -63,10 +61,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
return context.l10n.menuActionSort;
|
||||
case EntrySetAction.group:
|
||||
return context.l10n.menuActionGroup;
|
||||
case EntrySetAction.configureView:
|
||||
return context.l10n.menuActionConfigureView;
|
||||
case EntrySetAction.select:
|
||||
return context.l10n.menuActionSelect;
|
||||
case EntrySetAction.selectAll:
|
||||
|
@ -119,10 +115,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
IconData _getIconData() {
|
||||
switch (this) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
return AIcons.sort;
|
||||
case EntrySetAction.group:
|
||||
return AIcons.group;
|
||||
case EntrySetAction.configureView:
|
||||
return AIcons.view;
|
||||
case EntrySetAction.select:
|
||||
return AIcons.select;
|
||||
case EntrySetAction.selectAll:
|
||||
|
|
|
@ -44,12 +44,11 @@ class AlbumFilter extends CollectionFilter {
|
|||
String getTooltip(BuildContext context) => album;
|
||||
|
||||
@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(
|
||||
context: context,
|
||||
albumPath: album,
|
||||
size: size,
|
||||
embossed: embossed,
|
||||
) ??
|
||||
(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);
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -29,7 +29,7 @@ class FavouriteFilter extends CollectionFilter {
|
|||
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
|
||||
|
||||
@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
|
||||
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);
|
||||
|
||||
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)));
|
||||
|
||||
|
|
|
@ -58,15 +58,13 @@ class LocationFilter extends CollectionFilter {
|
|||
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location;
|
||||
|
||||
@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) {
|
||||
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) {
|
||||
return Text(
|
||||
flag,
|
||||
style: TextStyle(fontSize: size, shadows: const []),
|
||||
style: TextStyle(fontSize: size),
|
||||
textScaleFactor: 1.0,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ class MimeFilter extends CollectionFilter {
|
|||
}
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -64,7 +64,7 @@ class QueryFilter extends CollectionFilter {
|
|||
String get universalLabel => query;
|
||||
|
||||
@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
|
||||
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;
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -94,7 +94,7 @@ class TypeFilter extends CollectionFilter {
|
|||
}
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -19,6 +19,7 @@ class SettingsDefaults {
|
|||
static const mustBackTwiceToExit = true;
|
||||
static const keepScreenOn = KeepScreenOn.viewerOnly;
|
||||
static const homePage = HomePageSetting.collection;
|
||||
static const tileLayout = TileLayout.grid;
|
||||
|
||||
// drawer
|
||||
static final drawerTypeBookmarks = [
|
||||
|
|
|
@ -49,6 +49,7 @@ class Settings extends ChangeNotifier {
|
|||
static const homePageKey = 'home_page';
|
||||
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||
static const tileExtentPrefixKey = 'tile_extent_';
|
||||
static const tileLayoutPrefixKey = 'tile_layout_';
|
||||
|
||||
// drawer
|
||||
static const drawerTypeBookmarksKey = 'drawer_type_bookmarks';
|
||||
|
@ -247,6 +248,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
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
|
||||
|
||||
List<CollectionFilter?> get drawerTypeBookmarks =>
|
||||
|
@ -570,6 +575,12 @@ class Settings extends ChangeNotifier {
|
|||
} else {
|
||||
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 {
|
||||
switch (key) {
|
||||
case subtitleTextColorKey:
|
||||
|
|
|
@ -7,3 +7,5 @@ enum AlbumChipGroupFactor { none, importance, volume }
|
|||
enum EntrySortFactor { date, size, name }
|
||||
|
||||
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 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
|
||||
static const IconData add = Icons.add_circle_outline;
|
||||
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 geoBounds = Icons.public_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 import = MdiIcons.fileImportOutline;
|
||||
static const IconData info = Icons.info_outlined;
|
||||
|
@ -80,7 +84,6 @@ class AIcons {
|
|||
static const IconData setCover = MdiIcons.imageEditOutline;
|
||||
static const IconData share = Icons.share_outlined;
|
||||
static const IconData show = Icons.visibility_outlined;
|
||||
static const IconData sort = Icons.sort_outlined;
|
||||
static const IconData speed = Icons.speed_outlined;
|
||||
static const IconData stats = Icons.pie_chart_outlined;
|
||||
static const IconData streams = Icons.translate_outlined;
|
||||
|
@ -88,6 +91,7 @@ class AIcons {
|
|||
static const IconData streamAudio = Icons.audiotrack_outlined;
|
||||
static const IconData streamText = Icons.closed_caption_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 zoomOut = Icons.remove_outlined;
|
||||
static const IconData collapse = Icons.expand_less_outlined;
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
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
|
||||
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/theme/durations.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/filter_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/basic/menu.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:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -395,11 +396,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
Future<void> _onActionSelected(EntrySetAction action) async {
|
||||
switch (action) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
await _sort();
|
||||
break;
|
||||
case EntrySetAction.group:
|
||||
await _group();
|
||||
case EntrySetAction.configureView:
|
||||
await _configureView();
|
||||
break;
|
||||
case EntrySetAction.select:
|
||||
context.read<Selection<AvesEntry>>().select();
|
||||
|
@ -434,47 +432,42 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _sort() async {
|
||||
final value = await showDialog<EntrySortFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<EntrySortFactor>(
|
||||
initialValue: settings.collectionSortFactor,
|
||||
options: {
|
||||
EntrySortFactor.date: context.l10n.collectionSortDate,
|
||||
EntrySortFactor.size: context.l10n.collectionSortSize,
|
||||
EntrySortFactor.name: context.l10n.collectionSortName,
|
||||
},
|
||||
title: context.l10n.collectionSortTitle,
|
||||
),
|
||||
Future<void> _configureView() async {
|
||||
final initialValue = Tuple3(
|
||||
settings.collectionSortFactor,
|
||||
settings.collectionSectionFactor,
|
||||
settings.getTileLayout(CollectionPage.routeName),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.collectionSortFactor = value;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _group() async {
|
||||
final value = await showDialog<EntryGroupFactor>(
|
||||
final value = await showDialog<Tuple3<EntrySortFactor?, EntryGroupFactor?, TileLayout?>>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return AvesSelectionDialog<EntryGroupFactor>(
|
||||
initialValue: settings.collectionSectionFactor,
|
||||
options: {
|
||||
return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
|
||||
initialValue: initialValue,
|
||||
sortOptions: {
|
||||
EntrySortFactor.date: l10n.collectionSortDate,
|
||||
EntrySortFactor.size: l10n.collectionSortSize,
|
||||
EntrySortFactor.name: l10n.collectionSortName,
|
||||
},
|
||||
groupOptions: {
|
||||
EntryGroupFactor.album: l10n.collectionGroupAlbum,
|
||||
EntryGroupFactor.month: l10n.collectionGroupMonth,
|
||||
EntryGroupFactor.day: l10n.collectionGroupDay,
|
||||
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
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.collectionSectionFactor = value;
|
||||
if (value != null && initialValue != 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/filters/favourite.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/enums.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/widgets/collection/app_bar.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/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/insets.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/media_query.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/sliver.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.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/scaling.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -74,11 +76,13 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
|
||||
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
|
||||
return Consumer<CollectionLens>(
|
||||
builder: (context, collection, child) {
|
||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||
builder: (context, tileExtent, child) {
|
||||
builder: (context, thumbnailExtent, child) {
|
||||
return Selector<TileExtentController, Tuple3<double, int, double>>(
|
||||
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
|
||||
builder: (context, c, child) {
|
||||
|
@ -89,23 +93,28 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
final target = context.read<DurationsData>().staggeredAnimationPageTarget;
|
||||
final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
|
||||
return GridTheme(
|
||||
extent: tileExtent,
|
||||
extent: thumbnailExtent,
|
||||
child: EntryListDetailsTheme(
|
||||
extent: thumbnailExtent,
|
||||
child: SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileLayout: tileLayout,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
tileExtent: tileExtent,
|
||||
tileBuilder: (entry) => InteractiveThumbnail(
|
||||
tileExtent: thumbnailExtent,
|
||||
tileBuilder: (entry) => InteractiveTile(
|
||||
key: ValueKey(entry.contentId),
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
thumbnailExtent: thumbnailExtent,
|
||||
tileLayout: tileLayout,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
),
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
|
@ -115,6 +124,7 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
collection: collection,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
scrollController: PrimaryScrollController.of(context)!,
|
||||
tileLayout: tileLayout,
|
||||
),
|
||||
);
|
||||
return sectionedListLayoutProvider;
|
||||
|
@ -127,27 +137,28 @@ class _CollectionSectionedContent extends StatefulWidget {
|
|||
final CollectionLens collection;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
final ScrollController scrollController;
|
||||
final TileLayout tileLayout;
|
||||
|
||||
const _CollectionSectionedContent({
|
||||
required this.collection,
|
||||
required this.isScrollingNotifier,
|
||||
required this.scrollController,
|
||||
required this.tileLayout,
|
||||
});
|
||||
|
||||
@override
|
||||
_CollectionSectionedContentState createState() => _CollectionSectionedContentState();
|
||||
}
|
||||
|
||||
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> with WidgetsBindingObserver, GridItemTrackerMixin<AvesEntry, _CollectionSectionedContent> {
|
||||
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> {
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
@override
|
||||
TileLayout get tileLayout => widget.tileLayout;
|
||||
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
@override
|
||||
final ValueNotifier<double> appBarHeightNotifier = ValueNotifier(0);
|
||||
|
||||
@override
|
||||
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
|
||||
|
||||
@override
|
||||
|
@ -169,6 +180,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
|
|||
final scaler = _CollectionScaler(
|
||||
scrollableKey: scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
tileLayout: tileLayout,
|
||||
child: scrollView,
|
||||
);
|
||||
|
||||
|
@ -181,18 +193,26 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
|
|||
child: scaler,
|
||||
);
|
||||
|
||||
return selector;
|
||||
return GridItemTracker<AvesEntry>(
|
||||
scrollableKey: scrollableKey,
|
||||
tileLayout: tileLayout,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
scrollController: scrollController,
|
||||
child: selector,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CollectionScaler extends StatelessWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final TileLayout tileLayout;
|
||||
final Widget child;
|
||||
|
||||
const _CollectionScaler({
|
||||
required this.scrollableKey,
|
||||
required this.appBarHeightNotifier,
|
||||
required this.tileLayout,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
|
@ -201,10 +221,12 @@ class _CollectionScaler extends StatelessWidget {
|
|||
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||
return GridScaleGestureDetector<AvesEntry>(
|
||||
scrollableKey: scrollableKey,
|
||||
tileLayout: tileLayout,
|
||||
heightForWidth: (width) => width,
|
||||
gridBuilder: (center, tileSize, child) => CustomPaint(
|
||||
painter: GridPainter(
|
||||
center: center,
|
||||
tileLayout: tileLayout,
|
||||
tileCenter: center,
|
||||
tileSize: tileSize,
|
||||
spacing: tileSpacing,
|
||||
borderWidth: DecoratedThumbnail.borderWidth,
|
||||
|
@ -213,11 +235,13 @@ class _CollectionScaler extends StatelessWidget {
|
|||
),
|
||||
child: child,
|
||||
),
|
||||
scaledBuilder: (entry, tileSize) => DecoratedThumbnail(
|
||||
scaledBuilder: (entry, tileSize) => EntryListDetailsTheme(
|
||||
extent: tileSize.height,
|
||||
child: Tile(
|
||||
entry: entry,
|
||||
tileExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
selectable: false,
|
||||
highlightable: false,
|
||||
thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
tileLayout: tileLayout,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
|
|
@ -50,10 +50,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
}) {
|
||||
switch (action) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
case EntrySetAction.configureView:
|
||||
return true;
|
||||
case EntrySetAction.group:
|
||||
return sortFactor == EntrySortFactor.date;
|
||||
case EntrySetAction.select:
|
||||
return appMode.canSelect && !isSelecting;
|
||||
case EntrySetAction.selectAll:
|
||||
|
@ -97,8 +95,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
final hasSelection = selectedItemCount > 0;
|
||||
|
||||
switch (action) {
|
||||
case EntrySetAction.sort:
|
||||
case EntrySetAction.group:
|
||||
case EntrySetAction.configureView:
|
||||
return true;
|
||||
case EntrySetAction.select:
|
||||
return hasItems;
|
||||
|
@ -132,8 +129,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
void onActionSelected(BuildContext context, EntrySetAction action) {
|
||||
switch (action) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
case EntrySetAction.group:
|
||||
case EntrySetAction.configureView:
|
||||
case EntrySetAction.select:
|
||||
case EntrySetAction.selectAll:
|
||||
case EntrySetAction.selectNone:
|
||||
|
@ -226,7 +222,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
@ -274,19 +269,12 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
|
||||
Future<void> _move(BuildContext context, {required MoveType moveType}) async {
|
||||
final l10n = context.l10n;
|
||||
final source = context.read<CollectionSource>();
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
final selectedItems = _getExpandedSelectedItems(selection);
|
||||
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
||||
|
||||
final destinationAlbum = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<String>(
|
||||
settings: const RouteSettings(name: AlbumPickPage.routeName),
|
||||
builder: (context) => AlbumPickPage(source: source, moveType: moveType),
|
||||
),
|
||||
);
|
||||
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
|
||||
if (destinationAlbum == null) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
||||
if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
|
||||
|
@ -326,6 +314,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
nameConflictStrategy = value;
|
||||
}
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
showOpReport<MoveOpEvent>(
|
||||
|
@ -470,7 +459,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: l10n.unsupportedTypeDialogTitle,
|
||||
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
|
||||
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/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/widgets/collection/grid/headers/any.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
|
@ -12,6 +13,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
|
|||
Key? key,
|
||||
required this.collection,
|
||||
required double scrollableWidth,
|
||||
required TileLayout tileLayout,
|
||||
required int columnCount,
|
||||
required double spacing,
|
||||
required double tileExtent,
|
||||
|
@ -21,6 +23,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
|
|||
}) : super(
|
||||
key: key,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileLayout: tileLayout,
|
||||
columnCount: columnCount,
|
||||
spacing: spacing,
|
||||
tileWidth: tileExtent,
|
||||
|
|
|
@ -2,32 +2,36 @@ import 'package:aves/app_mode.dart';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/selection.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/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/scaling.dart';
|
||||
import 'package:aves/widgets/common/grid/scaling.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class InteractiveThumbnail extends StatelessWidget {
|
||||
class InteractiveTile extends StatelessWidget {
|
||||
final CollectionLens collection;
|
||||
final AvesEntry entry;
|
||||
final double tileExtent;
|
||||
final double thumbnailExtent;
|
||||
final TileLayout tileLayout;
|
||||
final ValueNotifier<bool>? isScrollingNotifier;
|
||||
|
||||
const InteractiveThumbnail({
|
||||
const InteractiveTile({
|
||||
Key? key,
|
||||
required this.collection,
|
||||
required this.entry,
|
||||
required this.tileExtent,
|
||||
required this.thumbnailExtent,
|
||||
required this.tileLayout,
|
||||
this.isScrollingNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
key: ValueKey(entry.uri),
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
||||
switch (appMode) {
|
||||
|
@ -51,13 +55,13 @@ class InteractiveThumbnail extends StatelessWidget {
|
|||
},
|
||||
child: MetaData(
|
||||
metaData: ScalerMetadata(entry),
|
||||
child: DecoratedThumbnail(
|
||||
child: Tile(
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
// 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,
|
||||
thumbnailExtent: thumbnailExtent,
|
||||
tileLayout: tileLayout,
|
||||
selectable: true,
|
||||
highlightable: true,
|
||||
isScrollingNotifier: isScrollingNotifier,
|
||||
// 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)
|
||||
// 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,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
|
@ -50,7 +50,6 @@ mixin PermissionAwareMixin {
|
|||
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
|
||||
final volume = dir.getVolumeDescription(context);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: context.l10n.storageAccessDialogTitle,
|
||||
content: Text(context.l10n.storageAccessDialogMessage(directory, volume)),
|
||||
actions: [
|
||||
|
@ -84,7 +83,6 @@ mixin PermissionAwareMixin {
|
|||
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
|
||||
final volume = dir.getVolumeDescription(context);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: context.l10n.restrictedAccessDialogTitle,
|
||||
content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)),
|
||||
actions: [
|
||||
|
|
|
@ -80,7 +80,6 @@ mixin SizeAwareMixin {
|
|||
final freeSize = formatFileSize(locale, free);
|
||||
final volume = destinationVolume.getDescription(context);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: l10n.notEnoughSpaceDialogTitle,
|
||||
content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)),
|
||||
actions: [
|
||||
|
|
|
@ -71,7 +71,6 @@ class _ColorPickerDialogState extends State<ColorPickerDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
scrollableContent: [
|
||||
ColorPicker(
|
||||
color: color,
|
||||
|
|
|
@ -2,21 +2,45 @@ import 'dart:async';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBindingObserver {
|
||||
ValueNotifier<double> get appBarHeightNotifier;
|
||||
class GridItemTracker<T> extends StatefulWidget {
|
||||
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 {
|
||||
final scrollableContext = scrollableKey.currentContext!;
|
||||
final scrollableContext = widget.scrollableKey.currentContext!;
|
||||
return (scrollableContext.findRenderObject() as RenderBox).size;
|
||||
}
|
||||
|
||||
|
@ -43,11 +67,27 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
|
|||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
void didUpdateWidget(covariant GridItemTracker<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.tileLayout != widget.tileLayout) {
|
||||
_onLayoutChange();
|
||||
}
|
||||
_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
|
||||
void dispose() {
|
||||
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 {
|
||||
// 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
|
||||
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
|
||||
// w.r.t. the `MediaQuery` update, and consequentially to this widget update
|
||||
// `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update
|
||||
void _onWindowOrientationChange() {
|
||||
void _onLayoutChange() {
|
||||
// do not track when view shows top edge
|
||||
if (scrollController.offset == 0) return;
|
||||
|
||||
final layout = _lastSectionedListLayout;
|
||||
final halfSize = _lastScrollableSize / 2;
|
||||
final center = Offset(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
|
@ -21,6 +22,7 @@ class ScalerMetadata<T> {
|
|||
|
||||
class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final TileLayout tileLayout;
|
||||
final double Function(double width) heightForWidth;
|
||||
final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
|
||||
final Widget Function(T item, Size tileSize) scaledBuilder;
|
||||
|
@ -30,6 +32,7 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
|
|||
const GridScaleGestureDetector({
|
||||
Key? key,
|
||||
required this.scrollableKey,
|
||||
required this.tileLayout,
|
||||
required this.heightForWidth,
|
||||
required this.gridBuilder,
|
||||
required this.scaledBuilder,
|
||||
|
@ -111,17 +114,29 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
_extentMax = tileExtentController.effectiveExtentMax;
|
||||
|
||||
final halfSize = _startSize! / 2;
|
||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
||||
final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => ScaleOverlay(
|
||||
builder: (scaledTileSize) => SizedBox.fromSize(
|
||||
builder: (context) => _ScaleOverlay(
|
||||
builder: (scaledTileSize) {
|
||||
late final double themeExtent;
|
||||
switch (widget.tileLayout) {
|
||||
case TileLayout.grid:
|
||||
themeExtent = scaledTileSize.width;
|
||||
break;
|
||||
case TileLayout.list:
|
||||
themeExtent = scaledTileSize.height;
|
||||
break;
|
||||
}
|
||||
return SizedBox.fromSize(
|
||||
size: scaledTileSize,
|
||||
child: GridTheme(
|
||||
extent: scaledTileSize.width,
|
||||
extent: themeExtent,
|
||||
child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
|
||||
),
|
||||
),
|
||||
center: thumbnailCenter,
|
||||
);
|
||||
},
|
||||
tileLayout: widget.tileLayout,
|
||||
center: tileCenter,
|
||||
viewportWidth: gridWidth,
|
||||
gridBuilder: widget.gridBuilder,
|
||||
scaledSizeNotifier: _scaledSizeNotifier!,
|
||||
|
@ -133,8 +148,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
if (_scaledSizeNotifier == null) return;
|
||||
final s = details.scale;
|
||||
switch (widget.tileLayout) {
|
||||
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) {
|
||||
|
@ -148,7 +171,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
final tileExtentController = context.read<TileExtentController>();
|
||||
final oldExtent = tileExtentController.extentNotifier.value;
|
||||
// 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;
|
||||
if (newExtent == oldExtent) {
|
||||
_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 TileLayout tileLayout;
|
||||
final Offset center;
|
||||
final double viewportWidth;
|
||||
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,
|
||||
required this.builder,
|
||||
required this.tileLayout,
|
||||
required this.center,
|
||||
required this.viewportWidth,
|
||||
required this.scaledSizeNotifier,
|
||||
|
@ -203,7 +237,7 @@ class ScaleOverlay extends StatefulWidget {
|
|||
_ScaleOverlayState createState() => _ScaleOverlayState();
|
||||
}
|
||||
|
||||
class _ScaleOverlayState extends State<ScaleOverlay> {
|
||||
class _ScaleOverlayState extends State<_ScaleOverlay> {
|
||||
bool _init = false;
|
||||
|
||||
Offset get center => widget.center;
|
||||
|
@ -222,26 +256,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
|
|||
child: Builder(
|
||||
builder: (context) => IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
decoration: _init
|
||||
? 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,
|
||||
],
|
||||
),
|
||||
),
|
||||
decoration: _buildBackgroundDecoration(context),
|
||||
duration: Durations.collectionScalingBackgroundAnimation,
|
||||
child: ValueListenableBuilder<Size>(
|
||||
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 {
|
||||
final Offset center;
|
||||
final TileLayout tileLayout;
|
||||
final Offset tileCenter;
|
||||
final Size tileSize;
|
||||
final double spacing, borderWidth;
|
||||
final Radius borderRadius;
|
||||
final Color color;
|
||||
|
||||
const GridPainter({
|
||||
required this.center,
|
||||
required this.tileLayout,
|
||||
required this.tileCenter,
|
||||
required this.tileSize,
|
||||
required this.spacing,
|
||||
required this.borderWidth,
|
||||
|
@ -301,15 +352,18 @@ class GridPainter extends CustomPainter {
|
|||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final tileWidth = tileSize.width;
|
||||
final tileHeight = tileSize.height;
|
||||
|
||||
final strokePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = borderWidth
|
||||
..shader = ui.Gradient.radial(
|
||||
center,
|
||||
tileWidth * 2,
|
||||
late final Offset chipCenter;
|
||||
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,
|
||||
|
@ -319,22 +373,52 @@ class GridPainter extends CustomPainter {
|
|||
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()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = borderWidth
|
||||
..shader = strokeShader;
|
||||
final fillPaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = color.withOpacity(.25);
|
||||
|
||||
final deltaX = tileWidth + spacing;
|
||||
final deltaY = tileHeight + spacing;
|
||||
for (var i = -2; i <= 2; i++) {
|
||||
final chipWidth = chipSize.width;
|
||||
final chipHeight = chipSize.height;
|
||||
|
||||
final deltaX = tileSize.width + spacing;
|
||||
final deltaY = tileSize.height + spacing;
|
||||
for (var i = -deltaColumn; i <= deltaColumn; i++) {
|
||||
final dx = deltaX * i;
|
||||
for (var j = -2; j <= 2; j++) {
|
||||
if (i == 0 && j == 0) continue;
|
||||
final dy = deltaY * j;
|
||||
final rect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: center + Offset(dx, dy),
|
||||
width: tileWidth,
|
||||
height: tileHeight,
|
||||
center: chipCenter + Offset(dx, dy),
|
||||
width: chipWidth - borderWidth,
|
||||
height: chipHeight - borderWidth,
|
||||
),
|
||||
borderRadius,
|
||||
);
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -12,6 +13,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
||||
final double scrollableWidth;
|
||||
final TileLayout tileLayout;
|
||||
final int columnCount;
|
||||
final double spacing, tileWidth, tileHeight;
|
||||
final Widget Function(T item) tileBuilder;
|
||||
|
@ -21,14 +23,17 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
const SectionedListLayoutProvider({
|
||||
Key? key,
|
||||
required this.scrollableWidth,
|
||||
required this.columnCount,
|
||||
required this.tileLayout,
|
||||
required int columnCount,
|
||||
required this.spacing,
|
||||
required this.tileWidth,
|
||||
required double tileWidth,
|
||||
required this.tileHeight,
|
||||
required this.tileBuilder,
|
||||
required this.tileAnimationDelay,
|
||||
required this.child,
|
||||
}) : assert(scrollableWidth != 0),
|
||||
columnCount = tileLayout == TileLayout.list ? 1 : columnCount,
|
||||
tileWidth = tileLayout == TileLayout.list ? scrollableWidth : tileWidth,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
|
|
|
@ -37,7 +37,7 @@ class AvesFilterDecoration {
|
|||
|
||||
class AvesFilterChip extends StatefulWidget {
|
||||
final CollectionFilter filter;
|
||||
final bool removable, showGenericIcon, useFilterColor;
|
||||
final bool removable, showText, showGenericIcon, useFilterColor;
|
||||
final AvesFilterDecoration? decoration;
|
||||
final String? banner;
|
||||
final Widget? leadingOverride, details;
|
||||
|
@ -60,6 +60,7 @@ class AvesFilterChip extends StatefulWidget {
|
|||
Key? key,
|
||||
required this.filter,
|
||||
this.removable = false,
|
||||
this.showText = true,
|
||||
this.showGenericIcon = true,
|
||||
this.useFilterColor = true,
|
||||
this.decoration,
|
||||
|
@ -160,14 +161,17 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final decoration = widget.decoration;
|
||||
final chipBackground = Theme.of(context).scaffoldBackgroundColor;
|
||||
|
||||
Widget? content;
|
||||
if (widget.showText) {
|
||||
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;
|
||||
Widget content = Row(
|
||||
content = Row(
|
||||
mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
@ -221,6 +225,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
|
||||
final banner = widget.banner;
|
||||
|
@ -244,7 +249,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
borderRadius: borderRadius,
|
||||
),
|
||||
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
|
||||
onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : 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/theme/icons.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:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -201,25 +199,9 @@ class IconUtils {
|
|||
required BuildContext context,
|
||||
required String albumPath,
|
||||
double? size,
|
||||
bool embossed = false,
|
||||
}) {
|
||||
size ??= IconTheme.of(context).size;
|
||||
Widget buildIcon(IconData icon) => embossed
|
||||
? 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,
|
||||
);
|
||||
Widget buildIcon(IconData icon) => Icon(icon, size: size);
|
||||
switch (androidFileUtils.getAlbumType(albumPath)) {
|
||||
case AlbumType.camera:
|
||||
return buildIcon(AIcons.cameraAlbum);
|
||||
|
|
|
@ -60,7 +60,6 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
final shortestSide = context.select<MediaQueryData, double>((mq) => mq.size.shortestSide);
|
||||
final extent = (shortestSide / 3.0).clamp(60.0, 160.0);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
scrollableContent: [
|
||||
if (_coverEntry != null)
|
||||
Container(
|
||||
|
|
|
@ -3,29 +3,39 @@ import 'dart:ui';
|
|||
import 'package:aves/widgets/common/extensions/build_context.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 controlCaptionPadding = 16;
|
||||
static const double borderWidth = 1.0;
|
||||
|
||||
AvesDialog({
|
||||
const AvesDialog({
|
||||
Key? key,
|
||||
required BuildContext context,
|
||||
String? title,
|
||||
ScrollController? scrollController,
|
||||
List<Widget>? scrollableContent,
|
||||
bool hasScrollBar = true,
|
||||
double horizontalContentPadding = defaultHorizontalContentPadding,
|
||||
Widget? content,
|
||||
required List<Widget> actions,
|
||||
this.title,
|
||||
this.scrollController,
|
||||
this.scrollableContent,
|
||||
this.hasScrollBar = true,
|
||||
this.horizontalContentPadding = defaultHorizontalContentPadding,
|
||||
this.content,
|
||||
required this.actions,
|
||||
}) : assert((scrollableContent != null) ^ (content != null)),
|
||||
super(
|
||||
key: key,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: title != null
|
||||
? Padding(
|
||||
// padding to avoid transparent border overlapping
|
||||
padding: const EdgeInsets.symmetric(horizontal: borderWidth),
|
||||
child: DialogTitle(title: title),
|
||||
child: DialogTitle(title: title!),
|
||||
)
|
||||
: null,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
|
@ -33,34 +43,26 @@ class AvesDialog extends AlertDialog {
|
|||
// 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),
|
||||
content: _buildContent(context),
|
||||
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)),
|
||||
),
|
||||
shape: shape(context),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildContent(
|
||||
BuildContext context,
|
||||
ScrollController? scrollController,
|
||||
List<Widget>? scrollableContent,
|
||||
bool hasScrollBar,
|
||||
Widget? content,
|
||||
) {
|
||||
Widget _buildContent(BuildContext context) {
|
||||
if (content != null) {
|
||||
return content;
|
||||
return content!;
|
||||
}
|
||||
|
||||
if (scrollableContent != null) {
|
||||
scrollController ??= ScrollController();
|
||||
final _scrollController = scrollController ?? ScrollController();
|
||||
|
||||
Widget child = ListView(
|
||||
controller: scrollController,
|
||||
controller: _scrollController,
|
||||
shrinkWrap: true,
|
||||
children: scrollableContent,
|
||||
children: scrollableContent!,
|
||||
);
|
||||
|
||||
if (hasScrollBar) {
|
||||
|
@ -75,7 +77,7 @@ class AvesDialog extends AlertDialog {
|
|||
),
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
controller: _scrollController,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
@ -89,11 +91,7 @@ class AvesDialog extends AlertDialog {
|
|||
// but the `ListView` viewport does not have one
|
||||
width: 1,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: Divider.createBorderSide(context, width: borderWidth),
|
||||
),
|
||||
),
|
||||
decoration: contentDecoration(context),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
@ -101,6 +99,21 @@ class AvesDialog extends AlertDialog {
|
|||
|
||||
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 {
|
||||
|
@ -116,11 +129,7 @@ class DialogTitle extends StatelessWidget {
|
|||
return Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: Divider.createBorderSide(context, width: AvesDialog.borderWidth),
|
||||
),
|
||||
),
|
||||
decoration: AvesDialog.contentDecoration(context),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
|
@ -138,7 +147,6 @@ void showNoMatchingAppDialog(BuildContext context) {
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: context.l10n.noMatchingAppDialogTitle,
|
||||
content: Text(context.l10n.noMatchingAppDialogMessage),
|
||||
actions: [
|
||||
|
|
|
@ -40,7 +40,6 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
|
|||
final confirmationButtonLabel = widget.confirmationButtonLabel;
|
||||
final needConfirmation = confirmationButtonLabel != null;
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: widget.title,
|
||||
scrollableContent: [
|
||||
if (message != null)
|
||||
|
|
|
@ -129,7 +129,6 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
),
|
||||
),
|
||||
child: AvesDialog(
|
||||
context: context,
|
||||
title: l10n.editEntryDateDialogTitle,
|
||||
scrollableContent: [
|
||||
setTile,
|
||||
|
@ -289,7 +288,6 @@ class _TimeShiftDialogState extends State<TimeShiftDialog> {
|
|||
Widget build(BuildContext context) {
|
||||
const textStyle = TextStyle(fontSize: 34);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
scrollableContent: [
|
||||
Center(
|
||||
child: Padding(
|
||||
|
|
|
@ -46,7 +46,6 @@ class _RemoveEntryMetadataDialogState extends State<RemoveEntryMetadataDialog> {
|
|||
final l10n = context.l10n;
|
||||
final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: l10n.removeEntryMetadataDialogTitle,
|
||||
scrollableContent: [
|
||||
..._mainOptions.map(_toTile),
|
||||
|
|
|
@ -41,7 +41,6 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
|
|
|
@ -33,7 +33,6 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
|
@ -4,11 +4,9 @@ import 'package:aves/model/source/collection_lens.dart';
|
|||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/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/dialogs/aves_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:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
@ -29,14 +27,13 @@ class CoverSelectionDialog extends StatefulWidget {
|
|||
|
||||
class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
||||
late bool _isCustom;
|
||||
AvesEntry? _customEntry, _recentEntry;
|
||||
AvesEntry? _customEntry;
|
||||
|
||||
CollectionFilter get filter => widget.filter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_recentEntry = context.read<CollectionSource>().recentEntry(filter);
|
||||
_customEntry = widget.customEntry;
|
||||
_isCustom = _customEntry != null;
|
||||
}
|
||||
|
@ -47,10 +44,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
child: Builder(
|
||||
builder: (context) {
|
||||
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(
|
||||
context: context,
|
||||
title: l10n.setCoverDialogTitle,
|
||||
scrollableContent: [
|
||||
...[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: [
|
||||
TextButton(
|
||||
|
|
|
@ -63,7 +63,6 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
}
|
||||
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: context.l10n.newAlbumDialogTitle,
|
||||
scrollController: _scrollController,
|
||||
scrollableContent: [
|
||||
|
|
|
@ -42,7 +42,6 @@ class _RenameAlbumDialogState extends State<RenameAlbumDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: ValueListenableBuilder<bool>(
|
||||
valueListenable: _existsNotifier,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
horizontalContentPadding: 4,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
|
@ -47,7 +47,6 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
|
|||
final canSelectText = _textStreams.length > 1;
|
||||
final canSelect = canSelectVideo || canSelectAudio || canSelectText;
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: canSelect ? null : Text(context.l10n.videoStreamSelectionDialogNoSelection),
|
||||
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/move_type.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
|
@ -24,6 +25,21 @@ import 'package:flutter/scheduler.dart';
|
|||
import 'package:provider/provider.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 {
|
||||
static const routeName = '/album_pick';
|
||||
|
||||
|
@ -47,7 +63,9 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, Tuple2<AlbumChipGroupFactor, ChipSortFactor>>(
|
||||
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||
value: ValueNotifier(AppMode.pickInternal),
|
||||
child: Selector<Settings, Tuple2<AlbumChipGroupFactor, ChipSortFactor>>(
|
||||
selector: (context, s) => Tuple2(s.albumGroupFactor, s.albumSortFactor),
|
||||
builder: (context, s, child) {
|
||||
return StreamBuilder(
|
||||
|
@ -79,13 +97,13 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
|
|||
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) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: ChipSetAction.sort,
|
||||
child: MenuRow(text: context.l10n.menuActionSort, icon: const Icon(AIcons.sort)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChipSetAction.group,
|
||||
child: MenuRow(text: context.l10n.menuActionGroup, icon: const Icon(AIcons.group)),
|
||||
value: ChipSetAction.configureView,
|
||||
child: MenuRow(text: context.l10n.menuActionConfigureView, icon: const Icon(AIcons.view)),
|
||||
),
|
||||
];
|
||||
},
|
||||
|
|
|
@ -17,14 +17,16 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.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/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:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
|
||||
final Iterable<FilterGridItem<AlbumFilter>> _items;
|
||||
|
@ -40,6 +42,12 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
|
|||
@override
|
||||
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
|
||||
bool isVisible(
|
||||
ChipSetAction action, {
|
||||
|
@ -49,8 +57,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
|
|||
required Set<AlbumFilter> selectedFilters,
|
||||
}) {
|
||||
switch (action) {
|
||||
case ChipSetAction.group:
|
||||
return true;
|
||||
case ChipSetAction.createAlbum:
|
||||
return appMode == AppMode.main && !isSelecting;
|
||||
case ChipSetAction.delete:
|
||||
|
@ -96,9 +102,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
|
|||
void onActionSelected(BuildContext context, Set<AlbumFilter> filters, ChipSetAction action) {
|
||||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.group:
|
||||
_group(context);
|
||||
break;
|
||||
case ChipSetAction.createAlbum:
|
||||
_createAlbum(context);
|
||||
break;
|
||||
|
@ -118,23 +121,42 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
|
|||
|
||||
void _browse(BuildContext context) => context.read<Selection<FilterGridItem<AlbumFilter>>>().browse();
|
||||
|
||||
Future<void> _group(BuildContext context) async {
|
||||
final factor = await showDialog<AlbumChipGroupFactor>(
|
||||
@override
|
||||
Future<void> configureView(BuildContext context) async {
|
||||
final initialValue = Tuple3(
|
||||
sortFactor,
|
||||
settings.albumGroupFactor,
|
||||
tileLayout,
|
||||
);
|
||||
final value = await showDialog<Tuple3<ChipSortFactor?, AlbumChipGroupFactor?, TileLayout?>>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<AlbumChipGroupFactor>(
|
||||
initialValue: settings.albumGroupFactor,
|
||||
options: {
|
||||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return TileViewDialog<ChipSortFactor, AlbumChipGroupFactor, TileLayout>(
|
||||
initialValue: initialValue,
|
||||
sortOptions: {
|
||||
ChipSortFactor.date: context.l10n.chipSortDate,
|
||||
ChipSortFactor.name: context.l10n.chipSortName,
|
||||
ChipSortFactor.count: context.l10n.chipSortCount,
|
||||
},
|
||||
groupOptions: {
|
||||
AlbumChipGroupFactor.importance: context.l10n.albumGroupTier,
|
||||
AlbumChipGroupFactor.volume: context.l10n.albumGroupVolume,
|
||||
AlbumChipGroupFactor.none: context.l10n.albumGroupNone,
|
||||
},
|
||||
title: context.l10n.albumGroupTitle,
|
||||
),
|
||||
layoutOptions: {
|
||||
TileLayout.grid: l10n.tileLayoutGrid,
|
||||
TileLayout.list: l10n.tileLayoutList,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (factor != null) {
|
||||
settings.albumGroupFactor = factor;
|
||||
if (value != null && initialValue != value) {
|
||||
sortFactor = value.item1!;
|
||||
settings.albumGroupFactor = value.item2!;
|
||||
tileLayout = value.item3!;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,7 +194,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
|
@ -35,7 +35,6 @@ class ChipActionDelegate {
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.hideFilterConfirmationDialogMessage),
|
||||
actions: [
|
||||
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/extensions/build_context.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/tile_view_dialog.dart';
|
||||
import 'package:aves/widgets/map/map_page.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.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);
|
||||
|
||||
TileLayout get tileLayout;
|
||||
|
||||
set tileLayout(TileLayout tileLayout);
|
||||
|
||||
bool isVisible(
|
||||
ChipSetAction action, {
|
||||
required AppMode appMode,
|
||||
|
@ -43,10 +47,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
final hasSelection = selectedFilters.isNotEmpty;
|
||||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
case ChipSetAction.configureView:
|
||||
return true;
|
||||
case ChipSetAction.group:
|
||||
return false;
|
||||
case ChipSetAction.select:
|
||||
return appMode.canSelect && !isSelecting;
|
||||
case ChipSetAction.selectAll:
|
||||
|
@ -91,8 +93,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
|
||||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
case ChipSetAction.group:
|
||||
case ChipSetAction.configureView:
|
||||
case ChipSetAction.select:
|
||||
case ChipSetAction.selectAll:
|
||||
case ChipSetAction.selectNone:
|
||||
|
@ -120,10 +121,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
void onActionSelected(BuildContext context, Set<T> filters, ChipSetAction action) {
|
||||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
_showSortDialog(context);
|
||||
break;
|
||||
case ChipSetAction.group:
|
||||
case ChipSetAction.configureView:
|
||||
configureView(context);
|
||||
break;
|
||||
case ChipSetAction.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)));
|
||||
}
|
||||
|
||||
Future<void> _showSortDialog(BuildContext context) async {
|
||||
final factor = await showDialog<ChipSortFactor>(
|
||||
Future<void> configureView(BuildContext context) async {
|
||||
final initialValue = Tuple3(
|
||||
sortFactor,
|
||||
null,
|
||||
tileLayout,
|
||||
);
|
||||
final value = await showDialog<Tuple3<ChipSortFactor?, void, TileLayout?>>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
|
||||
initialValue: sortFactor,
|
||||
options: {
|
||||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return TileViewDialog<ChipSortFactor, void, TileLayout>(
|
||||
initialValue: initialValue,
|
||||
sortOptions: {
|
||||
ChipSortFactor.date: context.l10n.chipSortDate,
|
||||
ChipSortFactor.name: context.l10n.chipSortName,
|
||||
ChipSortFactor.count: context.l10n.chipSortCount,
|
||||
},
|
||||
title: context.l10n.chipSortTitle,
|
||||
),
|
||||
layoutOptions: {
|
||||
TileLayout.grid: l10n.tileLayoutGrid,
|
||||
TileLayout.list: l10n.tileLayoutList,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (factor != null) {
|
||||
sortFactor = factor;
|
||||
if (value != null && initialValue != value) {
|
||||
sortFactor = value.item1!;
|
||||
tileLayout = value.item3!;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,7 +255,6 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.hideFilterConfirmationDialogMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:aves/model/filters/location.dart';
|
|||
import 'package:aves/model/settings/settings.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/countries_page.dart';
|
||||
|
||||
class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> {
|
||||
final Iterable<FilterGridItem<LocationFilter>> _items;
|
||||
|
@ -17,4 +18,10 @@ class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter>
|
|||
|
||||
@override
|
||||
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/source/enums.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> {
|
||||
final Iterable<FilterGridItem<TagFilter>> _items;
|
||||
|
@ -17,4 +18,10 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
|
|||
|
||||
@override
|
||||
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/icons.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/thumbnail/image.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:provider/provider.dart';
|
||||
|
||||
|
@ -24,7 +22,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
final T filter;
|
||||
final double extent, thumbnailExtent;
|
||||
final AvesEntry? coverEntry;
|
||||
final bool pinned;
|
||||
final bool showText, pinned;
|
||||
final String? banner;
|
||||
final FilterCallback? onTap;
|
||||
final HeroType heroType;
|
||||
|
@ -35,6 +33,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
required this.extent,
|
||||
double? thumbnailExtent,
|
||||
this.coverEntry,
|
||||
this.showText = true,
|
||||
this.pinned = false,
|
||||
this.banner,
|
||||
this.onTap,
|
||||
|
@ -42,11 +41,14 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
}) : thumbnailExtent = thumbnailExtent ?? extent,
|
||||
super(key: key);
|
||||
|
||||
static double tileHeight({required double extent, required double textScaleFactor}) {
|
||||
return extent + infoHeight(extent: extent, textScaleFactor: textScaleFactor);
|
||||
static double tileHeight({required double extent, required double textScaleFactor, required bool showText}) {
|
||||
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
|
||||
// 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;
|
||||
|
@ -106,13 +108,20 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
return AvesFilterChip(
|
||||
key: chipKey,
|
||||
filter: filter,
|
||||
showText: showText,
|
||||
showGenericIcon: false,
|
||||
decoration: AvesFilterDecoration(
|
||||
widget: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.textScaleFactor,
|
||||
builder: (context, textScaleFactor, child) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: infoHeight(extent: extent, textScaleFactor: textScaleFactor)),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: infoHeight(
|
||||
extent: extent,
|
||||
textScaleFactor: textScaleFactor,
|
||||
showText: showText,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
@ -142,7 +151,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
radius: radius(extent),
|
||||
),
|
||||
banner: banner,
|
||||
details: _buildDetails(source, filter),
|
||||
details: showText ? _buildDetails(source, filter) : null,
|
||||
padding: titlePadding,
|
||||
heroType: heroType,
|
||||
onTap: onTap,
|
||||
|
@ -161,10 +170,9 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
AnimatedPadding(
|
||||
padding: EdgeInsets.only(right: padding),
|
||||
duration: Durations.chipDecorationAnimation,
|
||||
child: DecoratedIcon(
|
||||
child: Icon(
|
||||
AIcons.pin,
|
||||
color: FilterGridPage.detailColor,
|
||||
shadows: Constants.embossShadows,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
|
@ -172,10 +180,9 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
AnimatedPadding(
|
||||
padding: EdgeInsets.only(right: padding),
|
||||
duration: Durations.chipDecorationAnimation,
|
||||
child: DecoratedIcon(
|
||||
child: Icon(
|
||||
AIcons.removableStorage,
|
||||
color: FilterGridPage.detailColor,
|
||||
shadows: Constants.embossShadows,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -7,12 +7,15 @@ import 'package:flutter/widgets.dart';
|
|||
class FilterChipGridDecorator<T extends CollectionFilter, U extends FilterGridItem<T>> extends StatelessWidget {
|
||||
final U gridItem;
|
||||
final double extent;
|
||||
final bool selectable, highlightable;
|
||||
final Widget child;
|
||||
|
||||
const FilterChipGridDecorator({
|
||||
Key? key,
|
||||
required this.gridItem,
|
||||
required this.extent,
|
||||
this.selectable = true,
|
||||
this.highlightable = true,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -26,11 +29,13 @@ class FilterChipGridDecorator<T extends CollectionFilter, U extends FilterGridIt
|
|||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
child,
|
||||
if (selectable)
|
||||
GridItemSelectionOverlay<FilterGridItem<T>>(
|
||||
item: gridItem,
|
||||
borderRadius: borderRadius,
|
||||
padding: EdgeInsets.all(extent / 24),
|
||||
),
|
||||
if (highlightable)
|
||||
ChipHighlightOverlay(
|
||||
filter: gridItem.filter,
|
||||
extent: extent,
|
||||
|
|
|
@ -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/media_query.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/selector.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/providers/media_query_data_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/drawer/app_drawer.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/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_layout.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -48,7 +49,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
final ValueNotifier<String> queryNotifier;
|
||||
final QueryTest<T>? applyQuery;
|
||||
final Widget Function() emptyBuilder;
|
||||
final FilterCallback onTap;
|
||||
final HeroType heroType;
|
||||
|
||||
const FilterGridPage({
|
||||
|
@ -64,7 +64,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
required this.queryNotifier,
|
||||
this.applyQuery,
|
||||
required this.emptyBuilder,
|
||||
required this.onTap,
|
||||
required this.heroType,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -103,7 +102,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
queryNotifier: queryNotifier,
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: emptyBuilder,
|
||||
onTap: onTap,
|
||||
heroType: heroType,
|
||||
),
|
||||
),
|
||||
|
@ -129,7 +127,6 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
|||
final ValueNotifier<String> queryNotifier;
|
||||
final QueryTest<T>? applyQuery;
|
||||
final Widget Function() emptyBuilder;
|
||||
final FilterCallback onTap;
|
||||
final HeroType heroType;
|
||||
|
||||
const FilterGrid({
|
||||
|
@ -145,7 +142,6 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
|||
required this.queryNotifier,
|
||||
required this.applyQuery,
|
||||
required this.emptyBuilder,
|
||||
required this.onTap,
|
||||
required this.heroType,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -183,7 +179,6 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
|
|||
queryNotifier: widget.queryNotifier,
|
||||
applyQuery: widget.applyQuery,
|
||||
emptyBuilder: widget.emptyBuilder,
|
||||
onTap: widget.onTap,
|
||||
heroType: widget.heroType,
|
||||
),
|
||||
);
|
||||
|
@ -199,7 +194,6 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
final ValueNotifier<String> queryNotifier;
|
||||
final Widget Function() emptyBuilder;
|
||||
final QueryTest<T>? applyQuery;
|
||||
final FilterCallback onTap;
|
||||
final HeroType heroType;
|
||||
|
||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||
|
@ -216,7 +210,6 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
required this.queryNotifier,
|
||||
required this.applyQuery,
|
||||
required this.emptyBuilder,
|
||||
required this.onTap,
|
||||
required this.heroType,
|
||||
}) : super(key: key) {
|
||||
_appBarHeightNotifier.value = appBarHeight;
|
||||
|
@ -224,6 +217,8 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
|
||||
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
|
||||
return ValueListenableBuilder<String>(
|
||||
valueListenable: queryNotifier,
|
||||
builder: (context, query, child) {
|
||||
|
@ -240,10 +235,9 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
});
|
||||
}
|
||||
|
||||
final pinnedFilters = settings.pinnedFilters;
|
||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||
builder: (context, tileExtent, child) {
|
||||
builder: (context, thumbnailExtent, child) {
|
||||
return Selector<TileExtentController, Tuple3<double, int, double>>(
|
||||
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
|
||||
builder: (context, c, child) {
|
||||
|
@ -256,39 +250,38 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
return Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.textScaleFactor,
|
||||
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(
|
||||
extent: tileExtent,
|
||||
extent: thumbnailExtent,
|
||||
child: FilterListDetailsTheme(
|
||||
extent: thumbnailExtent,
|
||||
child: SectionedFilterListLayoutProvider<T>(
|
||||
sections: visibleSections,
|
||||
showHeaders: showHeaders,
|
||||
tileLayout: tileLayout,
|
||||
scrollableWidth: scrollableWidth,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
tileWidth: tileExtent,
|
||||
tileWidth: thumbnailExtent,
|
||||
tileHeight: tileHeight,
|
||||
tileBuilder: (gridItem) {
|
||||
final filter = gridItem.filter;
|
||||
return MetaData(
|
||||
metaData: ScalerMetadata(gridItem),
|
||||
child: FilterChipGridDecorator<T, FilterGridItem<T>>(
|
||||
return InteractiveFilterTile(
|
||||
gridItem: gridItem,
|
||||
extent: tileExtent,
|
||||
child: CoveredFilterChip(
|
||||
key: Key(filter.key),
|
||||
filter: filter,
|
||||
extent: tileExtent,
|
||||
pinned: pinnedFilters.contains(filter),
|
||||
banner: newFilters.contains(filter) ? context.l10n.newFilterBanner : null,
|
||||
onTap: onTap,
|
||||
chipExtent: thumbnailExtent,
|
||||
thumbnailExtent: thumbnailExtent,
|
||||
tileLayout: tileLayout,
|
||||
banner: _getFilterBanner(context, gridItem.filter),
|
||||
heroType: heroType,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
|
@ -304,13 +297,20 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
sortFactor: sortFactor,
|
||||
selectable: selectable,
|
||||
emptyBuilder: emptyBuilder,
|
||||
bannerBuilder: _getFilterBanner,
|
||||
scrollController: PrimaryScrollController.of(context)!,
|
||||
tileLayout: tileLayout,
|
||||
),
|
||||
);
|
||||
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 {
|
||||
|
@ -320,7 +320,9 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
|
|||
final ChipSortFactor sortFactor;
|
||||
final bool selectable;
|
||||
final Widget Function() emptyBuilder;
|
||||
final String? Function(BuildContext context, T filter) bannerBuilder;
|
||||
final ScrollController scrollController;
|
||||
final TileLayout tileLayout;
|
||||
|
||||
const _FilterSectionedContent({
|
||||
required this.appBar,
|
||||
|
@ -329,27 +331,28 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
|
|||
required this.sortFactor,
|
||||
required this.selectable,
|
||||
required this.emptyBuilder,
|
||||
required this.bannerBuilder,
|
||||
required this.scrollController,
|
||||
required this.tileLayout,
|
||||
});
|
||||
|
||||
@override
|
||||
_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;
|
||||
|
||||
@override
|
||||
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
|
||||
|
||||
TileLayout get tileLayout => widget.tileLayout;
|
||||
|
||||
Map<ChipSectionKey, List<FilterGridItem<T>>> get visibleSections => widget.visibleSections;
|
||||
|
||||
Widget Function() get emptyBuilder => widget.emptyBuilder;
|
||||
|
||||
@override
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
@override
|
||||
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
|
||||
|
||||
@override
|
||||
|
@ -374,6 +377,8 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
final scaler = _FilterScaler<T>(
|
||||
scrollableKey: scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
tileLayout: tileLayout,
|
||||
bannerBuilder: widget.bannerBuilder,
|
||||
child: scrollView,
|
||||
);
|
||||
|
||||
|
@ -386,7 +391,13 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
child: scaler,
|
||||
);
|
||||
|
||||
return selector;
|
||||
return GridItemTracker<FilterGridItem<T>>(
|
||||
scrollableKey: scrollableKey,
|
||||
tileLayout: tileLayout,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
scrollController: scrollController,
|
||||
child: selector,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _checkInitHighlight() async {
|
||||
|
@ -405,43 +416,48 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final TileLayout tileLayout;
|
||||
final String? Function(BuildContext context, T filter) bannerBuilder;
|
||||
final Widget child;
|
||||
|
||||
const _FilterScaler({
|
||||
required this.scrollableKey,
|
||||
required this.appBarHeightNotifier,
|
||||
required this.tileLayout,
|
||||
required this.bannerBuilder,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pinnedFilters = settings.pinnedFilters;
|
||||
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||
final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor);
|
||||
return GridScaleGestureDetector<FilterGridItem<T>>(
|
||||
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(
|
||||
painter: GridPainter(
|
||||
center: center,
|
||||
tileLayout: tileLayout,
|
||||
tileCenter: center,
|
||||
tileSize: tileSize,
|
||||
spacing: tileSpacing,
|
||||
borderWidth: AvesFilterChip.outlineWidth,
|
||||
borderRadius: CoveredFilterChip.radius(tileSize.width),
|
||||
borderRadius: CoveredFilterChip.radius(tileSize.shortestSide),
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
scaledBuilder: (item, tileSize) {
|
||||
final filter = item.filter;
|
||||
return CoveredFilterChip(
|
||||
filter: filter,
|
||||
extent: tileSize.width,
|
||||
scaledBuilder: (item, tileSize) => FilterListDetailsTheme(
|
||||
extent: tileSize.height,
|
||||
child: FilterTile(
|
||||
gridItem: item,
|
||||
chipExtent: tileLayout == TileLayout.grid ? tileSize.width : tileSize.height,
|
||||
thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
pinned: pinnedFilters.contains(filter),
|
||||
heroType: HeroType.never,
|
||||
);
|
||||
},
|
||||
tileLayout: tileLayout,
|
||||
banner: bannerBuilder(context, item.filter),
|
||||
),
|
||||
),
|
||||
highlightItem: (item) => item.filter,
|
||||
child: child,
|
||||
);
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
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/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/providers/selection_provider.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();
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
final c = (b.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0)).compareTo(a.entry?.bestDate ?? DateTime.fromMillisecondsSinceEpoch(0));
|
||||
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/source/enums.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.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.showHeaders,
|
||||
required double scrollableWidth,
|
||||
required TileLayout tileLayout,
|
||||
required int columnCount,
|
||||
required double spacing,
|
||||
required double tileWidth,
|
||||
|
@ -21,6 +23,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
|
|||
}) : super(
|
||||
key: key,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileLayout: tileLayout,
|
||||
columnCount: columnCount,
|
||||
spacing: spacing,
|
||||
tileWidth: tileWidth,
|
||||
|
|
|
@ -104,7 +104,7 @@ class _AddressRowState extends State<_AddressRow> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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),
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
@ -175,7 +175,7 @@ class _DateRow extends StatelessWidget {
|
|||
return Row(
|
||||
children: [
|
||||
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),
|
||||
Text(
|
||||
dateText,
|
||||
|
|
|
@ -63,20 +63,9 @@ class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
|
|||
icon: const Icon(AIcons.add),
|
||||
label: context.l10n.settingsNavigationDrawerAddAlbum,
|
||||
onPressed: () async {
|
||||
final source = context.read<CollectionSource>();
|
||||
final album = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<String>(
|
||||
settings: const RouteSettings(name: AlbumPickPage.routeName),
|
||||
builder: (context) => AlbumPickPage(source: source, moveType: null),
|
||||
),
|
||||
);
|
||||
|
||||
if (album == null || album.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
widget.items.add(album);
|
||||
});
|
||||
final album = await pickAlbum(context: context, moveType: null);
|
||||
if (album == null) return;
|
||||
setState(() => widget.items.add(album));
|
||||
},
|
||||
)
|
||||
],
|
||||
|
|
|
@ -161,7 +161,6 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(1)),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
@ -197,15 +196,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
await source.init();
|
||||
unawaited(source.refresh());
|
||||
}
|
||||
final destinationAlbum = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<String>(
|
||||
settings: const RouteSettings(name: AlbumPickPage.routeName),
|
||||
builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export),
|
||||
),
|
||||
);
|
||||
|
||||
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
|
||||
if (destinationAlbum == null) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
||||
if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return;
|
||||
|
|
|
@ -62,7 +62,6 @@ abstract class AvesVideoController {
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
{}
|
||||
{
|
||||
"ru": [
|
||||
"menuActionConfigureView",
|
||||
"viewDialogTabLayout",
|
||||
"tileLayoutGrid",
|
||||
"tileLayoutList"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue