#35 albums/countries/tags: multiple selection
This commit is contained in:
parent
81ab903d39
commit
f2270cfb77
45 changed files with 1262 additions and 842 deletions
|
@ -272,8 +272,14 @@
|
|||
"renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists",
|
||||
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
||||
|
||||
"deleteAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this album and its item?} other{Are you sure you want to delete this album and its {count} items?}}",
|
||||
"@deleteAlbumConfirmationDialogMessage": {
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this album and its item?} other{Are you sure you want to delete this album and its {count} items?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete these albums and their item?} other{Are you sure you want to delete these albums and their {count} items?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
|
@ -630,11 +636,11 @@
|
|||
"@settingsSubtitleThemeBackgroundColor": {},
|
||||
"settingsSubtitleThemeBackgroundOpacity": "Background opacity",
|
||||
"@settingsSubtitleThemeBackgroundOpacity": {},
|
||||
"settingsSubtitleThemeTextAlignmentLeft": "Left",
|
||||
"settingsSubtitleThemeTextAlignmentLeft": "Left",
|
||||
"@settingsSubtitleThemeTextAlignmentLeft": {},
|
||||
"settingsSubtitleThemeTextAlignmentCenter": "Center",
|
||||
"@settingsSubtitleThemeTextAlignmentCenter": {},
|
||||
"settingsSubtitleThemeTextAlignmentRight": "Right",
|
||||
"settingsSubtitleThemeTextAlignmentRight": "Right",
|
||||
"@settingsSubtitleThemeTextAlignmentRight": {},
|
||||
|
||||
"settingsSectionPrivacy": "Privacy",
|
||||
|
|
|
@ -124,7 +124,8 @@
|
|||
"renameAlbumDialogLabel": "앨범 이름",
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "사용 중인 이름입니다",
|
||||
|
||||
"deleteAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
|
||||
"renameEntryDialogLabel": "이름",
|
||||
|
||||
|
@ -299,9 +300,9 @@
|
|||
"settingsSubtitleThemeTextOpacity": "글자 투명도",
|
||||
"settingsSubtitleThemeBackgroundColor": "배경 색상",
|
||||
"settingsSubtitleThemeBackgroundOpacity": "배경 투명도",
|
||||
"settingsSubtitleThemeTextAlignmentLeft": "왼쪽",
|
||||
"settingsSubtitleThemeTextAlignmentLeft": "왼쪽",
|
||||
"settingsSubtitleThemeTextAlignmentCenter": "가운데",
|
||||
"settingsSubtitleThemeTextAlignmentRight": "오른쪽",
|
||||
"settingsSubtitleThemeTextAlignmentRight": "오른쪽",
|
||||
|
||||
"settingsSectionPrivacy": "개인정보 보호",
|
||||
"settingsEnableCrashReport": "오류 보고서 보내기",
|
||||
|
|
|
@ -2,29 +2,16 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum ChipSetAction {
|
||||
group,
|
||||
sort,
|
||||
stats,
|
||||
}
|
||||
|
||||
enum ChipAction {
|
||||
delete,
|
||||
hide,
|
||||
pin,
|
||||
unpin,
|
||||
rename,
|
||||
setCover,
|
||||
goToAlbumPage,
|
||||
goToCountryPage,
|
||||
goToTagPage,
|
||||
hide,
|
||||
}
|
||||
|
||||
extension ExtraChipAction on ChipAction {
|
||||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return context.l10n.chipActionDelete;
|
||||
case ChipAction.goToAlbumPage:
|
||||
return context.l10n.chipActionGoToAlbumPage;
|
||||
case ChipAction.goToCountryPage:
|
||||
|
@ -33,21 +20,11 @@ extension ExtraChipAction on ChipAction {
|
|||
return context.l10n.chipActionGoToTagPage;
|
||||
case ChipAction.hide:
|
||||
return context.l10n.chipActionHide;
|
||||
case ChipAction.pin:
|
||||
return context.l10n.chipActionPin;
|
||||
case ChipAction.unpin:
|
||||
return context.l10n.chipActionUnpin;
|
||||
case ChipAction.rename:
|
||||
return context.l10n.chipActionRename;
|
||||
case ChipAction.setCover:
|
||||
return context.l10n.chipActionSetCover;
|
||||
}
|
||||
}
|
||||
|
||||
IconData getIcon() {
|
||||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return AIcons.delete;
|
||||
case ChipAction.goToAlbumPage:
|
||||
return AIcons.album;
|
||||
case ChipAction.goToCountryPage:
|
||||
|
@ -56,13 +33,6 @@ extension ExtraChipAction on ChipAction {
|
|||
return AIcons.tag;
|
||||
case ChipAction.hide:
|
||||
return AIcons.hide;
|
||||
case ChipAction.pin:
|
||||
case ChipAction.unpin:
|
||||
return AIcons.pin;
|
||||
case ChipAction.rename:
|
||||
return AIcons.rename;
|
||||
case ChipAction.setCover:
|
||||
return AIcons.setCover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
86
lib/model/actions/chip_set_actions.dart
Normal file
86
lib/model/actions/chip_set_actions.dart
Normal file
|
@ -0,0 +1,86 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum ChipSetAction {
|
||||
// general
|
||||
sort,
|
||||
group,
|
||||
select,
|
||||
selectAll,
|
||||
selectNone,
|
||||
stats,
|
||||
// single/multiple filters
|
||||
delete,
|
||||
hide,
|
||||
pin,
|
||||
unpin,
|
||||
// single filter
|
||||
rename,
|
||||
setCover,
|
||||
}
|
||||
|
||||
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.select:
|
||||
return context.l10n.collectionActionSelect;
|
||||
case ChipSetAction.selectAll:
|
||||
return context.l10n.collectionActionSelectAll;
|
||||
case ChipSetAction.selectNone:
|
||||
return context.l10n.collectionActionSelectNone;
|
||||
case ChipSetAction.stats:
|
||||
return context.l10n.menuActionStats;
|
||||
// single/multiple filters
|
||||
case ChipSetAction.delete:
|
||||
return context.l10n.chipActionDelete;
|
||||
case ChipSetAction.hide:
|
||||
return context.l10n.chipActionHide;
|
||||
case ChipSetAction.pin:
|
||||
return context.l10n.chipActionPin;
|
||||
case ChipSetAction.unpin:
|
||||
return context.l10n.chipActionUnpin;
|
||||
// single filter
|
||||
case ChipSetAction.rename:
|
||||
return context.l10n.chipActionRename;
|
||||
case ChipSetAction.setCover:
|
||||
return context.l10n.chipActionSetCover;
|
||||
}
|
||||
}
|
||||
|
||||
IconData? getIcon() {
|
||||
switch (this) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
return AIcons.sort;
|
||||
case ChipSetAction.group:
|
||||
return AIcons.group;
|
||||
case ChipSetAction.select:
|
||||
return AIcons.select;
|
||||
case ChipSetAction.selectAll:
|
||||
case ChipSetAction.selectNone:
|
||||
return null;
|
||||
case ChipSetAction.stats:
|
||||
return AIcons.stats;
|
||||
// single/multiple filters
|
||||
case ChipSetAction.delete:
|
||||
return AIcons.delete;
|
||||
case ChipSetAction.hide:
|
||||
return AIcons.hide;
|
||||
case ChipSetAction.pin:
|
||||
return AIcons.pin;
|
||||
case ChipSetAction.unpin:
|
||||
return AIcons.unpin;
|
||||
// single filter
|
||||
case ChipSetAction.rename:
|
||||
return AIcons.rename;
|
||||
case ChipSetAction.setCover:
|
||||
return AIcons.setCover;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -275,13 +275,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
return recentEntry(filter);
|
||||
}
|
||||
|
||||
void changeFilterVisibility(CollectionFilter filter, bool visible) {
|
||||
void changeFilterVisibility(Set<CollectionFilter> filters, bool visible) {
|
||||
final hiddenFilters = settings.hiddenFilters;
|
||||
if (visible) {
|
||||
hiddenFilters.remove(filter);
|
||||
hiddenFilters.removeAll(filters);
|
||||
} else {
|
||||
hiddenFilters.add(filter);
|
||||
settings.searchHistory = settings.searchHistory..remove(filter);
|
||||
hiddenFilters.addAll(filters);
|
||||
settings.searchHistory = settings.searchHistory..removeWhere(filters.contains);
|
||||
}
|
||||
settings.hiddenFilters = hiddenFilters;
|
||||
|
||||
|
@ -292,10 +292,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
updateLocations();
|
||||
updateTags();
|
||||
|
||||
eventBus.fire(FilterVisibilityChangedEvent(filter, visible));
|
||||
eventBus.fire(FilterVisibilityChangedEvent(filters, visible));
|
||||
|
||||
if (visible) {
|
||||
refreshMetadata(visibleEntries.where(filter.test).toSet());
|
||||
refreshMetadata(visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -319,10 +319,10 @@ class EntryMovedEvent {
|
|||
}
|
||||
|
||||
class FilterVisibilityChangedEvent {
|
||||
final CollectionFilter filter;
|
||||
final Set<CollectionFilter> filters;
|
||||
final bool visible;
|
||||
|
||||
const FilterVisibilityChangedEvent(this.filter, this.visible);
|
||||
const FilterVisibilityChangedEvent(this.filters, this.visible);
|
||||
}
|
||||
|
||||
class ProgressEvent {
|
||||
|
|
|
@ -50,6 +50,7 @@ class AIcons {
|
|||
static const IconData layers = Icons.layers_outlined;
|
||||
static const IconData openOutside = Icons.open_in_new_outlined;
|
||||
static const IconData pin = Icons.push_pin_outlined;
|
||||
static const IconData unpin = MdiIcons.pinOffOutline;
|
||||
static const IconData play = Icons.play_arrow;
|
||||
static const IconData pause = Icons.pause;
|
||||
static const IconData print = Icons.print_outlined;
|
||||
|
|
|
@ -46,7 +46,6 @@ class CollectionAppBar extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin {
|
||||
final TextEditingController _searchFieldController = TextEditingController();
|
||||
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
||||
late AnimationController _browseToSelectAnimation;
|
||||
late Future<bool> _canAddShortcutsLoader;
|
||||
|
@ -83,7 +82,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
_unregisterWidget(widget);
|
||||
_isSelectingNotifier.removeListener(_onActivityChange);
|
||||
_browseToSelectAnimation.dispose();
|
||||
_searchFieldController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -271,7 +269,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
_browseToSelectAnimation.forward();
|
||||
} else {
|
||||
_browseToSelectAnimation.reverse();
|
||||
_searchFieldController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -370,13 +367,14 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
|
||||
void _goToSearch() {
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
source: collection.source,
|
||||
parentCollection: collection,
|
||||
),
|
||||
));
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
source: collection.source,
|
||||
parentCollection: collection,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _goToStats() {
|
||||
|
|
|
@ -14,7 +14,6 @@ import 'package:aves/widgets/collection/draggable_thumb_label.dart';
|
|||
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/collection/grid/thumbnail.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/theme.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';
|
||||
|
@ -23,6 +22,7 @@ import 'package:aves/widgets/common/extensions/media_query.dart';
|
|||
import 'package:aves/widgets/common/grid/item_tracker.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';
|
||||
|
@ -79,7 +79,7 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||
builder: (context, tileExtent, child) {
|
||||
return ThumbnailTheme(
|
||||
return GridTheme(
|
||||
extent: tileExtent,
|
||||
child: Selector<TileExtentController, Tuple3<double, int, double>>(
|
||||
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
|
||||
|
@ -173,7 +173,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
|
|||
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
|
||||
final selector = GridSelectionGestureDetector(
|
||||
selectable: isMainMode,
|
||||
entries: collection.sortedEntries,
|
||||
items: collection.sortedEntries,
|
||||
scrollController: scrollController,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
child: scaler,
|
||||
|
@ -210,14 +210,11 @@ class _CollectionScaler extends StatelessWidget {
|
|||
),
|
||||
child: child,
|
||||
),
|
||||
scaledBuilder: (entry, extent) => ThumbnailTheme(
|
||||
extent: extent,
|
||||
child: DecoratedThumbnail(
|
||||
entry: entry,
|
||||
tileExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
selectable: false,
|
||||
highlightable: false,
|
||||
),
|
||||
scaledBuilder: (entry, extent) => DecoratedThumbnail(
|
||||
entry: entry,
|
||||
tileExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
selectable: false,
|
||||
highlightable: false,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -33,7 +34,7 @@ class AlbumSectionHeader extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
return SectionHeader(
|
||||
return SectionHeader<AvesEntry>(
|
||||
sectionKey: EntryAlbumSectionKey(directory),
|
||||
leading: albumIcon,
|
||||
title: albumName ?? context.l10n.sectionUnknown,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
|
@ -39,9 +40,9 @@ class CollectionSectionHeader extends StatelessWidget {
|
|||
case EntryGroupFactor.album:
|
||||
return _buildAlbumHeader(context);
|
||||
case EntryGroupFactor.month:
|
||||
return MonthSectionHeader(key: ValueKey(sectionKey), date: (sectionKey as EntryDateSectionKey).date);
|
||||
return MonthSectionHeader<AvesEntry>(key: ValueKey(sectionKey), date: (sectionKey as EntryDateSectionKey).date);
|
||||
case EntryGroupFactor.day:
|
||||
return DaySectionHeader(key: ValueKey(sectionKey), date: (sectionKey as EntryDateSectionKey).date);
|
||||
return DaySectionHeader<AvesEntry>(key: ValueKey(sectionKey), date: (sectionKey as EntryDateSectionKey).date);
|
||||
case EntryGroupFactor.none:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/widgets/common/grid/header.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DaySectionHeader extends StatelessWidget {
|
||||
class DaySectionHeader<T> extends StatelessWidget {
|
||||
final DateTime? date;
|
||||
|
||||
const DaySectionHeader({
|
||||
|
@ -45,14 +45,14 @@ class DaySectionHeader extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionHeader(
|
||||
return SectionHeader<T>(
|
||||
sectionKey: EntryDateSectionKey(date),
|
||||
title: _formatDate(context, date),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MonthSectionHeader extends StatelessWidget {
|
||||
class MonthSectionHeader<T> extends StatelessWidget {
|
||||
final DateTime? date;
|
||||
|
||||
const MonthSectionHeader({
|
||||
|
@ -71,7 +71,7 @@ class MonthSectionHeader extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionHeader(
|
||||
return SectionHeader<T>(
|
||||
sectionKey: EntryDateSectionKey(date),
|
||||
title: _formatDate(context, date),
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:aves/model/source/collection_lens.dart';
|
|||
import 'package:aves/widgets/collection/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/overlay.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/common/grid/overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DecoratedThumbnail extends StatelessWidget {
|
||||
|
@ -46,7 +47,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
|||
children: [
|
||||
child,
|
||||
if (!isSvg) ThumbnailEntryOverlay(entry: entry),
|
||||
if (selectable) ThumbnailSelectionOverlay(entry: entry),
|
||||
if (selectable) GridItemSelectionOverlay(item: entry),
|
||||
if (highlightable) ThumbnailHighlightOverlay(entry: entry),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -2,11 +2,8 @@ import 'dart:math';
|
|||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/theme.dart';
|
||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -22,7 +19,7 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = [
|
||||
if (entry.hasGps && context.select<ThumbnailThemeData, bool>((t) => t.showLocation)) const GpsIcon(),
|
||||
if (entry.hasGps && context.select<GridThemeData, bool>((t) => t.showLocation)) const GpsIcon(),
|
||||
if (entry.isVideo)
|
||||
VideoIcon(
|
||||
entry: entry,
|
||||
|
@ -30,7 +27,7 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
|||
else if (entry.isAnimated)
|
||||
const AnimatedImageIcon()
|
||||
else ...[
|
||||
if (entry.isRaw && context.select<ThumbnailThemeData, bool>((t) => t.showRaw)) const RawIcon(),
|
||||
if (entry.isRaw && context.select<GridThemeData, bool>((t) => t.showRaw)) const RawIcon(),
|
||||
if (entry.isMultiPage) MultiPageIcon(entry: entry),
|
||||
if (entry.isGeotiff) const GeotiffIcon(),
|
||||
if (entry.is360) const SphericalImageIcon(),
|
||||
|
@ -46,57 +43,6 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class ThumbnailSelectionOverlay extends StatelessWidget {
|
||||
final AvesEntry entry;
|
||||
|
||||
static const duration = Durations.thumbnailOverlayAnimation;
|
||||
|
||||
const ThumbnailSelectionOverlay({
|
||||
Key? key,
|
||||
required this.entry,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSelecting = context.select<Selection<AvesEntry>, bool>((selection) => selection.isSelecting);
|
||||
final child = isSelecting
|
||||
? Selector<Selection<AvesEntry>, bool>(
|
||||
selector: (context, selection) => selection.isSelected([entry]),
|
||||
builder: (context, isSelected, child) {
|
||||
var child = isSelecting
|
||||
? OverlayIcon(
|
||||
key: ValueKey(isSelected),
|
||||
icon: isSelected ? AIcons.selected : AIcons.unselected,
|
||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
child = AnimatedSwitcher(
|
||||
duration: duration,
|
||||
switchInCurve: Curves.easeOutBack,
|
||||
switchOutCurve: Curves.easeOutBack,
|
||||
transitionBuilder: (child, animation) => ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
child = AnimatedContainer(
|
||||
duration: duration,
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
color: isSelected ? Colors.black54 : Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
return child;
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
return AnimatedSwitcher(
|
||||
duration: duration,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThumbnailHighlightOverlay extends StatefulWidget {
|
||||
final AvesEntry entry;
|
||||
|
||||
|
@ -125,7 +71,7 @@ class _ThumbnailHighlightOverlayState extends State<ThumbnailHighlightOverlay> {
|
|||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: Theme.of(context).accentColor,
|
||||
width: context.select<ThumbnailThemeData, double>((t) => t.highlightBorderWidth),
|
||||
width: context.select<GridThemeData, double>((t) => t.highlightBorderWidth),
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
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/section_keys.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SectionHeader extends StatelessWidget {
|
||||
class SectionHeader<T> extends StatelessWidget {
|
||||
final SectionKey sectionKey;
|
||||
final Widget? leading, trailing;
|
||||
final String title;
|
||||
|
@ -44,7 +43,7 @@ class SectionHeader extends StatelessWidget {
|
|||
children: [
|
||||
WidgetSpan(
|
||||
alignment: widgetSpanAlignment,
|
||||
child: _SectionSelectableLeading(
|
||||
child: _SectionSelectableLeading<T>(
|
||||
selectable: selectable,
|
||||
sectionKey: sectionKey,
|
||||
browsingBuilder: leading != null
|
||||
|
@ -78,9 +77,8 @@ class SectionHeader extends StatelessWidget {
|
|||
}
|
||||
|
||||
void _toggleSectionSelection(BuildContext context) {
|
||||
final collection = context.read<CollectionLens>();
|
||||
final sectionEntries = collection.sections[sectionKey]!;
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
final sectionEntries = context.read<SectionedListLayout<T>>().sections[sectionKey]!;
|
||||
final selection = context.read<Selection<T>>();
|
||||
final isSelected = selection.isSelected(sectionEntries);
|
||||
if (isSelected) {
|
||||
selection.removeFromSelection(sectionEntries);
|
||||
|
@ -124,7 +122,7 @@ class SectionHeader extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _SectionSelectableLeading extends StatelessWidget {
|
||||
class _SectionSelectableLeading<T> extends StatelessWidget {
|
||||
final bool selectable;
|
||||
final SectionKey sectionKey;
|
||||
final WidgetBuilder? browsingBuilder;
|
||||
|
@ -144,9 +142,9 @@ class _SectionSelectableLeading extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
if (!selectable) return _buildBrowsing(context);
|
||||
|
||||
final isSelecting = context.select<Selection<AvesEntry>, bool>((selection) => selection.isSelecting);
|
||||
final isSelecting = context.select<Selection<T>, bool>((selection) => selection.isSelecting);
|
||||
final Widget child = isSelecting
|
||||
? _SectionSelectingLeading(
|
||||
? _SectionSelectingLeading<T>(
|
||||
sectionKey: sectionKey,
|
||||
onPressed: onPressed,
|
||||
)
|
||||
|
@ -179,7 +177,7 @@ class _SectionSelectableLeading extends StatelessWidget {
|
|||
Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? const SizedBox(height: leadingDimension);
|
||||
}
|
||||
|
||||
class _SectionSelectingLeading extends StatelessWidget {
|
||||
class _SectionSelectingLeading<T> extends StatelessWidget {
|
||||
final SectionKey sectionKey;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
|
@ -191,8 +189,8 @@ class _SectionSelectingLeading extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sectionEntries = context.watch<CollectionLens>().sections[sectionKey]!;
|
||||
final selection = context.watch<Selection<AvesEntry>>();
|
||||
final sectionEntries = context.watch<SectionedListLayout<T>>().sections[sectionKey]!;
|
||||
final selection = context.watch<Selection<T>>();
|
||||
final isSelected = selection.isSelected(sectionEntries);
|
||||
return AnimatedSwitcher(
|
||||
duration: Durations.sectionHeaderAnimation,
|
||||
|
|
66
lib/widgets/common/grid/overlay.dart
Normal file
66
lib/widgets/common/grid/overlay.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GridItemSelectionOverlay<T> extends StatelessWidget {
|
||||
final T item;
|
||||
final BorderRadius? borderRadius;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
static const duration = Durations.thumbnailOverlayAnimation;
|
||||
|
||||
const GridItemSelectionOverlay({
|
||||
Key? key,
|
||||
required this.item,
|
||||
this.borderRadius,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSelecting = context.select<Selection<T>, bool>((selection) => selection.isSelecting);
|
||||
final child = isSelecting
|
||||
? Selector<Selection<T>, bool>(
|
||||
selector: (context, selection) => selection.isSelected([item]),
|
||||
builder: (context, isSelected, child) {
|
||||
var child = isSelecting
|
||||
? OverlayIcon(
|
||||
key: ValueKey(isSelected),
|
||||
icon: isSelected ? AIcons.selected : AIcons.unselected,
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
child = AnimatedSwitcher(
|
||||
duration: duration,
|
||||
switchInCurve: Curves.easeOutBack,
|
||||
switchOutCurve: Curves.easeOutBack,
|
||||
transitionBuilder: (child, animation) => ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
child = AnimatedContainer(
|
||||
duration: duration,
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.black54 : Colors.transparent,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
return child;
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
return AnimatedSwitcher(
|
||||
duration: duration,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
class GridSelectionGestureDetector<T> extends StatefulWidget {
|
||||
final bool selectable;
|
||||
final List<T> entries;
|
||||
final List<T> items;
|
||||
final ScrollController scrollController;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final Widget child;
|
||||
|
@ -19,7 +19,7 @@ class GridSelectionGestureDetector<T> extends StatefulWidget {
|
|||
const GridSelectionGestureDetector({
|
||||
Key? key,
|
||||
this.selectable = true,
|
||||
required this.entries,
|
||||
required this.items,
|
||||
required this.scrollController,
|
||||
required this.appBarHeightNotifier,
|
||||
required this.child,
|
||||
|
@ -37,7 +37,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
|
|||
late double _scrollSpeedFactor;
|
||||
Timer? _updateTimer;
|
||||
|
||||
List<T> get entries => widget.entries;
|
||||
List<T> get items => widget.items;
|
||||
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
|
@ -49,16 +49,17 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectable = widget.selectable;
|
||||
return GestureDetector(
|
||||
onLongPressStart: widget.selectable
|
||||
onLongPressStart: selectable
|
||||
? (details) {
|
||||
final fromEntry = _getEntryAt(details.localPosition);
|
||||
if (fromEntry == null) return;
|
||||
final fromItem = _getItemAt(details.localPosition);
|
||||
if (fromItem == null) return;
|
||||
|
||||
final selection = context.read<Selection<T>>();
|
||||
selection.toggleSelection(fromEntry);
|
||||
_selecting = selection.isSelected([fromEntry]);
|
||||
_fromIndex = entries.indexOf(fromEntry);
|
||||
selection.toggleSelection(fromItem);
|
||||
_selecting = selection.isSelected([fromItem]);
|
||||
_fromIndex = items.indexOf(fromItem);
|
||||
_lastToIndex = _fromIndex;
|
||||
_scrollableInsets = EdgeInsets.only(
|
||||
top: appBarHeight,
|
||||
|
@ -68,20 +69,29 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
|
|||
_pressing = true;
|
||||
}
|
||||
: null,
|
||||
onLongPressMoveUpdate: widget.selectable
|
||||
onLongPressMoveUpdate: selectable
|
||||
? (details) {
|
||||
if (!_pressing) return;
|
||||
_localPosition = details.localPosition;
|
||||
_onLongPressUpdate();
|
||||
}
|
||||
: null,
|
||||
onLongPressEnd: widget.selectable
|
||||
onLongPressEnd: selectable
|
||||
? (details) {
|
||||
if (!_pressing) return;
|
||||
_setScrollSpeed(0);
|
||||
_pressing = false;
|
||||
}
|
||||
: null,
|
||||
onTapUp: selectable && context.select<Selection<T>, bool>((selection) => selection.isSelecting)
|
||||
? (details) {
|
||||
final item = _getItemAt(details.localPosition);
|
||||
if (item == null) return;
|
||||
|
||||
final selection = context.read<Selection<T>>();
|
||||
selection.toggleSelection(item);
|
||||
}
|
||||
: null,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
@ -100,9 +110,9 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
|
|||
_setScrollSpeed(0);
|
||||
}
|
||||
|
||||
final toEntry = _getEntryAt(_localPosition);
|
||||
if (toEntry != null) {
|
||||
_toggleSelectionToIndex(entries.indexOf(toEntry));
|
||||
final toItem = _getItemAt(_localPosition);
|
||||
if (toItem != null) {
|
||||
_toggleSelectionToIndex(items.indexOf(toItem));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,16 +136,16 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
|
|||
duration: Duration(milliseconds: millis.round()),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
// use a timer to update the entry selection, because `onLongPressMoveUpdate`
|
||||
// use a timer to update the selection, because `onLongPressMoveUpdate`
|
||||
// is not called when the pointer stays still while the view is scrolling
|
||||
_updateTimer = Timer.periodic(scrollUpdateInterval, (_) => _onLongPressUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
T? _getEntryAt(Offset localPosition) {
|
||||
T? _getItemAt(Offset localPosition) {
|
||||
// as of Flutter v1.22.5, `hitTest` on the `ScrollView` render object works fine when it is static,
|
||||
// but when it is scrolling (through controller animation), result is incomplete and children are missing,
|
||||
// so we use custom layout computation instead to find the entry.
|
||||
// so we use custom layout computation instead to find the item.
|
||||
final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition;
|
||||
final sectionedListLayout = context.read<SectionedListLayout<T>>();
|
||||
return sectionedListLayout.getItemAt(offset);
|
||||
|
@ -148,26 +158,26 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
|
|||
if (_selecting) {
|
||||
if (toIndex <= _fromIndex) {
|
||||
if (toIndex < _lastToIndex) {
|
||||
selection.addToSelection(entries.getRange(toIndex, min(_fromIndex, _lastToIndex)));
|
||||
selection.addToSelection(items.getRange(toIndex, min(_fromIndex, _lastToIndex)));
|
||||
if (_fromIndex < _lastToIndex) {
|
||||
selection.removeFromSelection(entries.getRange(_fromIndex + 1, _lastToIndex + 1));
|
||||
selection.removeFromSelection(items.getRange(_fromIndex + 1, _lastToIndex + 1));
|
||||
}
|
||||
} else if (_lastToIndex < toIndex) {
|
||||
selection.removeFromSelection(entries.getRange(_lastToIndex, toIndex));
|
||||
selection.removeFromSelection(items.getRange(_lastToIndex, toIndex));
|
||||
}
|
||||
} else if (_fromIndex < toIndex) {
|
||||
if (_lastToIndex < toIndex) {
|
||||
selection.addToSelection(entries.getRange(max(_fromIndex, _lastToIndex), toIndex + 1));
|
||||
selection.addToSelection(items.getRange(max(_fromIndex, _lastToIndex), toIndex + 1));
|
||||
if (_lastToIndex < _fromIndex) {
|
||||
selection.removeFromSelection(entries.getRange(_lastToIndex, _fromIndex));
|
||||
selection.removeFromSelection(items.getRange(_lastToIndex, _fromIndex));
|
||||
}
|
||||
} else if (toIndex < _lastToIndex) {
|
||||
selection.removeFromSelection(entries.getRange(toIndex + 1, _lastToIndex + 1));
|
||||
selection.removeFromSelection(items.getRange(toIndex + 1, _lastToIndex + 1));
|
||||
}
|
||||
}
|
||||
_lastToIndex = toIndex;
|
||||
} else {
|
||||
selection.removeFromSelection(entries.getRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1));
|
||||
selection.removeFromSelection(items.getRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ThumbnailTheme extends StatelessWidget {
|
||||
class GridTheme extends StatelessWidget {
|
||||
final double extent;
|
||||
final bool? showLocation;
|
||||
final Widget child;
|
||||
|
||||
const ThumbnailTheme({
|
||||
const GridTheme({
|
||||
Key? key,
|
||||
required this.extent,
|
||||
this.showLocation,
|
||||
|
@ -18,12 +18,12 @@ class ThumbnailTheme extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ProxyProvider<Settings, ThumbnailThemeData>(
|
||||
return ProxyProvider<Settings, GridThemeData>(
|
||||
update: (_, settings, __) {
|
||||
final iconSize = min(28.0, (extent / 4)).roundToDouble();
|
||||
final fontSize = (iconSize / 2).floorToDouble();
|
||||
final highlightBorderWidth = extent * .1;
|
||||
return ThumbnailThemeData(
|
||||
return GridThemeData(
|
||||
iconSize: iconSize,
|
||||
fontSize: fontSize,
|
||||
highlightBorderWidth: highlightBorderWidth,
|
||||
|
@ -37,11 +37,11 @@ class ThumbnailTheme extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class ThumbnailThemeData {
|
||||
class GridThemeData {
|
||||
final double iconSize, fontSize, highlightBorderWidth;
|
||||
final bool showLocation, showRaw, showVideoDuration;
|
||||
|
||||
const ThumbnailThemeData({
|
||||
const GridThemeData({
|
||||
required this.iconSize,
|
||||
required this.fontSize,
|
||||
required this.highlightBorderWidth,
|
|
@ -8,7 +8,7 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/basic/menu_row.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -209,48 +209,44 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
borderRadius: borderRadius,
|
||||
child: widget.background,
|
||||
),
|
||||
Tooltip(
|
||||
message: filter.getTooltip(context),
|
||||
preferBelow: false,
|
||||
child: Material(
|
||||
color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: InkWell(
|
||||
// as of Flutter v1.22.5, `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
|
||||
? () {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) => onTap!(filter));
|
||||
setState(() => _tapped = true);
|
||||
}
|
||||
: null,
|
||||
onLongPress: onLongPress != null ? () => onLongPress!(context, filter, _tapPosition!) : null,
|
||||
borderRadius: borderRadius,
|
||||
child: FutureBuilder<Color>(
|
||||
future: _colorFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
_outlineColor = snapshot.data!;
|
||||
Material(
|
||||
color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: InkWell(
|
||||
// as of Flutter v1.22.5, `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
|
||||
? () {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) => onTap!(filter));
|
||||
setState(() => _tapped = true);
|
||||
}
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: _outlineColor,
|
||||
width: AvesFilterChip.outlineWidth,
|
||||
)),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
position: DecorationPosition.foreground,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
: null,
|
||||
onLongPress: onLongPress != null ? () => onLongPress!(context, filter, _tapPosition!) : null,
|
||||
borderRadius: borderRadius,
|
||||
child: FutureBuilder<Color>(
|
||||
future: _colorFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
_outlineColor = snapshot.data!;
|
||||
}
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: _outlineColor,
|
||||
width: AvesFilterChip.outlineWidth,
|
||||
)),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
position: DecorationPosition.foreground,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -5,7 +5,7 @@ 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/collection/thumbnail/theme.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';
|
||||
|
@ -20,11 +20,11 @@ class VideoIcon extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thumbnailTheme = context.watch<ThumbnailThemeData>();
|
||||
final showDuration = thumbnailTheme.showVideoDuration;
|
||||
final gridTheme = context.watch<GridThemeData>();
|
||||
final showDuration = gridTheme.showVideoDuration;
|
||||
Widget child = OverlayIcon(
|
||||
icon: entry.is360 ? AIcons.threeSixty : AIcons.videoThumb,
|
||||
size: thumbnailTheme.iconSize,
|
||||
size: gridTheme.iconSize,
|
||||
text: showDuration ? entry.durationText : null,
|
||||
iconScale: entry.is360 && showDuration ? .9 : 1,
|
||||
);
|
||||
|
@ -32,7 +32,7 @@ class VideoIcon extends StatelessWidget {
|
|||
child = DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade200,
|
||||
fontSize: thumbnailTheme.fontSize,
|
||||
fontSize: gridTheme.fontSize,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
@ -48,7 +48,7 @@ class AnimatedImageIcon extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return OverlayIcon(
|
||||
icon: AIcons.animated,
|
||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
iconScale: .8,
|
||||
);
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class GeotiffIcon extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return OverlayIcon(
|
||||
icon: AIcons.geo,
|
||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class SphericalImageIcon extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return OverlayIcon(
|
||||
icon: AIcons.threeSixty,
|
||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ class GpsIcon extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return OverlayIcon(
|
||||
icon: AIcons.location,
|
||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ class RawIcon extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return OverlayIcon(
|
||||
icon: AIcons.raw,
|
||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ class MultiPageIcon extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return OverlayIcon(
|
||||
icon: entry.isMotionPhoto ? AIcons.motionPhoto : AIcons.multiPage,
|
||||
size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
iconScale: .8,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -76,8 +76,6 @@ class AvesLogoPainter extends CustomPainter {
|
|||
path3.relativeArcToPoint(Offset(dim * 1.917, dim * -4.63), radius: Radius.circular(dim * 2.712), rotation: 112.5, clockwise: false);
|
||||
path3.close();
|
||||
|
||||
|
||||
|
||||
canvas.drawPath(
|
||||
path0,
|
||||
Paint()
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:ui' as ui;
|
|||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -79,7 +80,10 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
builder: (extent) => SizedBox(
|
||||
width: extent,
|
||||
height: extent,
|
||||
child: widget.scaledBuilder(_metadata!.item, extent),
|
||||
child: GridTheme(
|
||||
extent: extent,
|
||||
child: widget.scaledBuilder(_metadata!.item, extent),
|
||||
),
|
||||
),
|
||||
center: thumbnailCenter,
|
||||
viewportWidth: gridWidth,
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:aves/widgets/common/extensions/build_context.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/decorated_filter_chip.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';
|
||||
|
@ -89,7 +89,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: DecoratedFilterChip(
|
||||
child: CoveredFilterChip(
|
||||
filter: filter,
|
||||
extent: extent,
|
||||
coverEntry: _isCustom ? _customEntry : _recentEntry,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:aves/model/actions/chip_actions.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';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
|
@ -12,9 +13,10 @@ import 'package:aves/widgets/common/basic/menu_row.dart';
|
|||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/create_album_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -46,37 +48,41 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget appBar = AlbumPickAppBar(
|
||||
source: source,
|
||||
moveType: widget.moveType,
|
||||
actionDelegate: AlbumChipSetActionDelegate(),
|
||||
queryNotifier: _queryNotifier,
|
||||
);
|
||||
|
||||
return Selector<Settings, Tuple2<AlbumChipGroupFactor, ChipSortFactor>>(
|
||||
selector: (context, s) => Tuple2(s.albumGroupFactor, s.albumSortFactor),
|
||||
builder: (context, s, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterGridPage<AlbumFilter>(
|
||||
settingsRouteKey: AlbumListPage.routeName,
|
||||
appBar: appBar,
|
||||
appBarHeight: AlbumPickAppBar.preferredHeight,
|
||||
filterSections: AlbumListPage.getAlbumEntries(context, source),
|
||||
sortFactor: settings.albumSortFactor,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
queryNotifier: _queryNotifier,
|
||||
applyQuery: (filters, query) {
|
||||
if (query.isEmpty) return filters;
|
||||
query = query.toUpperCase();
|
||||
return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
|
||||
},
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
),
|
||||
onTap: (filter) => Navigator.pop<String>(context, (filter as AlbumFilter).album),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = AlbumListPage.getAlbumGridItems(context, source);
|
||||
return SelectionProvider<FilterGridItem<AlbumFilter>>(
|
||||
child: FilterGridPage<AlbumFilter>(
|
||||
settingsRouteKey: AlbumListPage.routeName,
|
||||
appBar: AlbumPickAppBar(
|
||||
source: source,
|
||||
moveType: widget.moveType,
|
||||
actionDelegate: AlbumChipSetActionDelegate(gridItems),
|
||||
queryNotifier: _queryNotifier,
|
||||
),
|
||||
appBarHeight: AlbumPickAppBar.preferredHeight,
|
||||
sections: AlbumListPage.groupToSections(context, gridItems),
|
||||
sortFactor: settings.albumSortFactor,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
selectable: false,
|
||||
queryNotifier: _queryNotifier,
|
||||
applyQuery: (filters, query) {
|
||||
if (query.isEmpty) return filters;
|
||||
query = query.toUpperCase();
|
||||
return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList();
|
||||
},
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
),
|
||||
onTap: (filter) => Navigator.pop<String>(context, (filter as AlbumFilter).album),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -156,7 +162,7 @@ class AlbumPickAppBar extends StatelessWidget {
|
|||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => actionDelegate.onActionSelected(context, action));
|
||||
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => actionDelegate.onActionSelected(context, {}, action));
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:aves/model/actions/chip_actions.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -9,8 +8,7 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -38,32 +36,22 @@ class AlbumListPage extends StatelessWidget {
|
|||
animation: androidFileUtils.appNameChangeNotifier,
|
||||
builder: (context, child) => StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
|
||||
source: source,
|
||||
title: context.l10n.albumPageTitle,
|
||||
sortFactor: settings.albumSortFactor,
|
||||
groupable: true,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
chipSetActionDelegate: AlbumChipSetActionDelegate(),
|
||||
chipActionDelegate: AlbumChipActionDelegate(),
|
||||
chipActionsBuilder: (filter) {
|
||||
final dir = VolumeRelativeDirectory.fromPath(filter.album);
|
||||
// do not allow renaming volume root
|
||||
final canRename = dir != null && dir.relativeDir.isNotEmpty;
|
||||
return [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.setCover,
|
||||
if (canRename) ChipAction.rename,
|
||||
ChipAction.delete,
|
||||
ChipAction.hide,
|
||||
];
|
||||
},
|
||||
filterSections: getAlbumEntries(context, source),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = getAlbumGridItems(context, source);
|
||||
return FilterNavigationPage<AlbumFilter>(
|
||||
source: source,
|
||||
title: context.l10n.albumPageTitle,
|
||||
sortFactor: settings.albumSortFactor,
|
||||
groupable: true,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
actionDelegate: AlbumChipSetActionDelegate(gridItems),
|
||||
filterSections: groupToSections(context, gridItems),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -72,14 +60,13 @@ class AlbumListPage extends StatelessWidget {
|
|||
|
||||
// common with album selection page to move/copy entries
|
||||
|
||||
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> getAlbumEntries(BuildContext context, CollectionSource source) {
|
||||
static List<FilterGridItem<AlbumFilter>> getAlbumGridItems(BuildContext context, CollectionSource source) {
|
||||
final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getAlbumDisplayName(context, album))).toSet();
|
||||
|
||||
final sorted = FilterNavigationPage.sort(settings.albumSortFactor, source, filters);
|
||||
return _group(context, sorted);
|
||||
return FilterNavigationPage.sort(settings.albumSortFactor, source, filters);
|
||||
}
|
||||
|
||||
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> _group(BuildContext context, Iterable<FilterGridItem<AlbumFilter>> sortedMapEntries) {
|
||||
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> groupToSections(BuildContext context, Iterable<FilterGridItem<AlbumFilter>> sortedMapEntries) {
|
||||
final pinned = settings.pinnedFilters.whereType<AlbumFilter>();
|
||||
final byPin = groupBy<FilterGridItem<AlbumFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter));
|
||||
final pinnedMapEntries = byPin[true] ?? [];
|
||||
|
|
|
@ -1,143 +1,112 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/actions/chip_actions.dart';
|
||||
import 'package:aves/model/actions/chip_set_actions.dart';
|
||||
import 'package:aves/model/actions/move_type.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/services/image_op_events.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
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/cover_selection_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/rename_album_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ChipActionDelegate {
|
||||
void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) {
|
||||
switch (action) {
|
||||
case ChipAction.pin:
|
||||
settings.pinnedFilters = settings.pinnedFilters..add(filter);
|
||||
break;
|
||||
case ChipAction.unpin:
|
||||
settings.pinnedFilters = settings.pinnedFilters..remove(filter);
|
||||
break;
|
||||
case ChipAction.hide:
|
||||
_hide(context, filter);
|
||||
break;
|
||||
case ChipAction.setCover:
|
||||
_showCoverSelectionDialog(context, filter);
|
||||
break;
|
||||
case ChipAction.goToAlbumPage:
|
||||
_goTo(context, filter, AlbumListPage.routeName, (context) => const AlbumListPage());
|
||||
break;
|
||||
case ChipAction.goToCountryPage:
|
||||
_goTo(context, filter, CountryListPage.routeName, (context) => const CountryListPage());
|
||||
break;
|
||||
case ChipAction.goToTagPage:
|
||||
_goTo(context, filter, TagListPage.routeName, (context) => const TagListPage());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
|
||||
final Iterable<FilterGridItem<AlbumFilter>> _items;
|
||||
|
||||
Future<void> _hide(BuildContext context, CollectionFilter filter) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.hideFilterConfirmationDialogMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(context.l10n.hideButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirmed == null || !confirmed) return;
|
||||
AlbumChipSetActionDelegate(Iterable<FilterGridItem<AlbumFilter>> items) : _items = items;
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.changeFilterVisibility(filter, false);
|
||||
}
|
||||
|
||||
void _showCoverSelectionDialog(BuildContext context, CollectionFilter filter) async {
|
||||
final contentId = covers.coverContentId(filter);
|
||||
final customEntry = context.read<CollectionSource>().visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
|
||||
final coverSelection = await showDialog<Tuple2<bool, AvesEntry?>>(
|
||||
context: context,
|
||||
builder: (context) => CoverSelectionDialog(
|
||||
filter: filter,
|
||||
customEntry: customEntry,
|
||||
),
|
||||
);
|
||||
if (coverSelection == null) return;
|
||||
|
||||
final isCustom = coverSelection.item1;
|
||||
await covers.set(filter, isCustom ? coverSelection.item2?.contentId : null);
|
||||
}
|
||||
|
||||
void _goTo(
|
||||
BuildContext context,
|
||||
CollectionFilter filter,
|
||||
String routeName,
|
||||
WidgetBuilder pageBuilder,
|
||||
) {
|
||||
context.read<HighlightInfo>().set(filter);
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: pageBuilder,
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
@override
|
||||
void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) {
|
||||
super.onActionSelected(context, filter, action);
|
||||
Iterable<FilterGridItem<AlbumFilter>> get allItems => _items;
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.albumSortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor;
|
||||
|
||||
@override
|
||||
bool isValid(Set<AlbumFilter> filters, ChipSetAction action) {
|
||||
switch (action) {
|
||||
case ChipAction.delete:
|
||||
_showDeleteDialog(context, filter as AlbumFilter);
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.rename:
|
||||
return true;
|
||||
default:
|
||||
return super.isValid(filters, action);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool canApply(Set<AlbumFilter> filters, ChipSetAction action) {
|
||||
switch (action) {
|
||||
case ChipSetAction.rename:
|
||||
{
|
||||
if (filters.length != 1) return false;
|
||||
// do not allow renaming volume root
|
||||
final dir = VolumeRelativeDirectory.fromPath(filters.first.album);
|
||||
return dir != null && dir.relativeDir.isNotEmpty;
|
||||
}
|
||||
default:
|
||||
return super.canApply(filters, action);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onActionSelected(BuildContext context, Set<AlbumFilter> filters, ChipSetAction action) {
|
||||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.group:
|
||||
_showGroupDialog(context);
|
||||
break;
|
||||
case ChipAction.rename:
|
||||
_showRenameDialog(context, filter as AlbumFilter);
|
||||
// single/multiple filters
|
||||
case ChipSetAction.delete:
|
||||
_showDeleteDialog(context, filters);
|
||||
break;
|
||||
// single filter
|
||||
case ChipSetAction.rename:
|
||||
_showRenameDialog(context, filters.first);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.onActionSelected(context, filters, action);
|
||||
}
|
||||
|
||||
Future<void> _showDeleteDialog(BuildContext context, AlbumFilter filter) async {
|
||||
Future<void> _showGroupDialog(BuildContext context) async {
|
||||
final factor = await showDialog<AlbumChipGroupFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<AlbumChipGroupFactor>(
|
||||
initialValue: settings.albumGroupFactor,
|
||||
options: {
|
||||
AlbumChipGroupFactor.importance: context.l10n.albumGroupTier,
|
||||
AlbumChipGroupFactor.volume: context.l10n.albumGroupVolume,
|
||||
AlbumChipGroupFactor.none: context.l10n.albumGroupNone,
|
||||
},
|
||||
title: context.l10n.albumGroupTitle,
|
||||
),
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showDeleteDialog(BuildContext context, Set<AlbumFilter> filters) async {
|
||||
final l10n = context.l10n;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final source = context.read<CollectionSource>();
|
||||
final album = filter.album;
|
||||
final todoEntries = source.visibleEntries.where(filter.test).toSet();
|
||||
final albums = filters.map((v) => v.album).toSet();
|
||||
final todoEntries = source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet();
|
||||
final todoCount = todoEntries.length;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
|
@ -145,7 +114,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(l10n.deleteAlbumConfirmationDialogMessage(todoCount)),
|
||||
content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
|
@ -161,7 +130,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
);
|
||||
if (confirmed == null || !confirmed) return;
|
||||
|
||||
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, albums)) return;
|
||||
|
||||
source.pauseMonitoring();
|
||||
showOpReport<ImageOpEvent>(
|
||||
|
@ -180,7 +149,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
}
|
||||
|
||||
// cleanup
|
||||
await storageService.deleteEmptyDirectories({album});
|
||||
await storageService.deleteEmptyDirectories(albums);
|
||||
},
|
||||
);
|
||||
}
|
75
lib/widgets/filter_grids/common/action_delegates/chip.dart
Normal file
75
lib/widgets/filter_grids/common/action_delegates/chip.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'package:aves/model/actions/chip_actions.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ChipActionDelegate {
|
||||
void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) {
|
||||
switch (action) {
|
||||
case ChipAction.hide:
|
||||
_hide(context, filter);
|
||||
break;
|
||||
case ChipAction.goToAlbumPage:
|
||||
_goTo(context, filter, AlbumListPage.routeName, (context) => const AlbumListPage());
|
||||
break;
|
||||
case ChipAction.goToCountryPage:
|
||||
_goTo(context, filter, CountryListPage.routeName, (context) => const CountryListPage());
|
||||
break;
|
||||
case ChipAction.goToTagPage:
|
||||
_goTo(context, filter, TagListPage.routeName, (context) => const TagListPage());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _hide(BuildContext context, CollectionFilter filter) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.hideFilterConfirmationDialogMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(context.l10n.hideButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirmed == null || !confirmed) return;
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.changeFilterVisibility({filter}, false);
|
||||
}
|
||||
|
||||
void _goTo(
|
||||
BuildContext context,
|
||||
CollectionFilter filter,
|
||||
String routeName,
|
||||
WidgetBuilder pageBuilder,
|
||||
) {
|
||||
context.read<HighlightInfo>().set(filter);
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: pageBuilder,
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
180
lib/widgets/filter_grids/common/action_delegates/chip_set.dart
Normal file
180
lib/widgets/filter_grids/common/action_delegates/chip_set.dart
Normal file
|
@ -0,0 +1,180 @@
|
|||
import 'package:aves/model/actions/chip_set_actions.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry.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_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
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/cover_selection_dialog.dart';
|
||||
import 'package:aves/widgets/stats/stats.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';
|
||||
|
||||
abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
Iterable<FilterGridItem<T>> get allItems;
|
||||
|
||||
ChipSortFactor get sortFactor;
|
||||
|
||||
set sortFactor(ChipSortFactor factor);
|
||||
|
||||
bool isValid(Set<T> filters, ChipSetAction action) {
|
||||
final hasSelection = filters.isNotEmpty;
|
||||
switch (action) {
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.rename:
|
||||
return false;
|
||||
case ChipSetAction.pin:
|
||||
return !hasSelection || !settings.pinnedFilters.containsAll(filters);
|
||||
case ChipSetAction.unpin:
|
||||
return hasSelection && settings.pinnedFilters.containsAll(filters);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool canApply(Set<T> filters, ChipSetAction action) {
|
||||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
case ChipSetAction.group:
|
||||
case ChipSetAction.select:
|
||||
case ChipSetAction.selectAll:
|
||||
case ChipSetAction.selectNone:
|
||||
case ChipSetAction.stats:
|
||||
return true;
|
||||
// single/multiple filters
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.hide:
|
||||
case ChipSetAction.pin:
|
||||
case ChipSetAction.unpin:
|
||||
return filters.isNotEmpty;
|
||||
// single filter
|
||||
case ChipSetAction.rename:
|
||||
case ChipSetAction.setCover:
|
||||
return filters.length == 1;
|
||||
}
|
||||
}
|
||||
|
||||
void onActionSelected(BuildContext context, Set<T> filters, ChipSetAction action) {
|
||||
switch (action) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
_showSortDialog(context);
|
||||
break;
|
||||
case ChipSetAction.stats:
|
||||
_goToStats(context);
|
||||
break;
|
||||
case ChipSetAction.select:
|
||||
context.read<Selection<FilterGridItem<T>>>().select();
|
||||
break;
|
||||
case ChipSetAction.selectAll:
|
||||
context.read<Selection<FilterGridItem<T>>>().addToSelection(allItems);
|
||||
break;
|
||||
case ChipSetAction.selectNone:
|
||||
context.read<Selection<FilterGridItem<T>>>().clearSelection();
|
||||
break;
|
||||
// single/multiple filters
|
||||
case ChipSetAction.pin:
|
||||
settings.pinnedFilters = settings.pinnedFilters..addAll(filters);
|
||||
break;
|
||||
case ChipSetAction.unpin:
|
||||
settings.pinnedFilters = settings.pinnedFilters..removeAll(filters);
|
||||
break;
|
||||
case ChipSetAction.hide:
|
||||
_hide(context, filters);
|
||||
break;
|
||||
// single filter
|
||||
case ChipSetAction.setCover:
|
||||
_showCoverSelectionDialog(context, filters.first);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showSortDialog(BuildContext context) async {
|
||||
final factor = await showDialog<ChipSortFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
|
||||
initialValue: sortFactor,
|
||||
options: {
|
||||
ChipSortFactor.date: context.l10n.chipSortDate,
|
||||
ChipSortFactor.name: context.l10n.chipSortName,
|
||||
ChipSortFactor.count: context.l10n.chipSortCount,
|
||||
},
|
||||
title: context.l10n.chipSortTitle,
|
||||
),
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
void _goToStats(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: StatsPage.routeName),
|
||||
builder: (context) => StatsPage(
|
||||
source: source,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _hide(BuildContext context, Set<T> filters) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.hideFilterConfirmationDialogMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(context.l10n.hideButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirmed == null || !confirmed) return;
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.changeFilterVisibility(filters, false);
|
||||
}
|
||||
|
||||
void _showCoverSelectionDialog(BuildContext context, T filter) async {
|
||||
final contentId = covers.coverContentId(filter);
|
||||
final customEntry = context.read<CollectionSource>().visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
|
||||
final coverSelection = await showDialog<Tuple2<bool, AvesEntry?>>(
|
||||
context: context,
|
||||
builder: (context) => CoverSelectionDialog(
|
||||
filter: filter,
|
||||
customEntry: customEntry,
|
||||
),
|
||||
);
|
||||
if (coverSelection == null) return;
|
||||
|
||||
final isCustom = coverSelection.item1;
|
||||
await covers.set(filter, isCustom ? coverSelection.item2?.contentId : null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
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';
|
||||
|
||||
class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> {
|
||||
final Iterable<FilterGridItem<LocationFilter>> _items;
|
||||
|
||||
CountryChipSetActionDelegate(Iterable<FilterGridItem<LocationFilter>> items) : _items = items;
|
||||
|
||||
@override
|
||||
Iterable<FilterGridItem<LocationFilter>> get allItems => _items;
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.countrySortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
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';
|
||||
|
||||
class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
|
||||
final Iterable<FilterGridItem<TagFilter>> _items;
|
||||
|
||||
TagChipSetActionDelegate(Iterable<FilterGridItem<TagFilter>> items) : _items = items;
|
||||
|
||||
@override
|
||||
Iterable<FilterGridItem<TagFilter>> get allItems => _items;
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.tagSortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor;
|
||||
}
|
239
lib/widgets/filter_grids/common/app_bar.dart
Normal file
239
lib/widgets/filter_grids/common/app_bar.dart
Normal file
|
@ -0,0 +1,239 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/actions/chip_set_actions.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
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_row.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
import 'package:aves/widgets/search/search_button.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilterGridAppBar<T extends CollectionFilter> extends StatefulWidget {
|
||||
final CollectionSource source;
|
||||
final String title;
|
||||
final ChipSetActionDelegate actionDelegate;
|
||||
final bool groupable, isEmpty;
|
||||
|
||||
const FilterGridAppBar({
|
||||
Key? key,
|
||||
required this.source,
|
||||
required this.title,
|
||||
required this.actionDelegate,
|
||||
required this.groupable,
|
||||
required this.isEmpty,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FilterGridAppBarState createState() => _FilterGridAppBarState<T>();
|
||||
}
|
||||
|
||||
class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGridAppBar<T>> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _browseToSelectAnimation;
|
||||
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
||||
|
||||
CollectionSource get source => widget.source;
|
||||
|
||||
ChipSetActionDelegate get actionDelegate => widget.actionDelegate;
|
||||
|
||||
static const filterSelectionActions = [
|
||||
ChipSetAction.setCover,
|
||||
ChipSetAction.pin,
|
||||
ChipSetAction.unpin,
|
||||
ChipSetAction.delete,
|
||||
ChipSetAction.rename,
|
||||
ChipSetAction.hide,
|
||||
];
|
||||
static const buttonActionCount = 2;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_browseToSelectAnimation = AnimationController(
|
||||
duration: Durations.iconAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_isSelectingNotifier.addListener(_onActivityChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isSelectingNotifier.removeListener(_onActivityChange);
|
||||
_browseToSelectAnimation.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||
final selection = context.watch<Selection<FilterGridItem<T>>>();
|
||||
final isSelecting = selection.isSelecting;
|
||||
_isSelectingNotifier.value = isSelecting;
|
||||
return SliverAppBar(
|
||||
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
|
||||
title: _buildAppBarTitle(isSelecting),
|
||||
actions: _buildActions(appMode, selection),
|
||||
titleSpacing: 0,
|
||||
floating: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBarLeading(bool isSelecting) {
|
||||
VoidCallback? onPressed;
|
||||
String? tooltip;
|
||||
if (isSelecting) {
|
||||
onPressed = () => context.read<Selection<FilterGridItem<T>>>().browse();
|
||||
tooltip = MaterialLocalizations.of(context).backButtonTooltip;
|
||||
} else {
|
||||
onPressed = Scaffold.of(context).openDrawer;
|
||||
tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip;
|
||||
}
|
||||
return IconButton(
|
||||
key: const Key('appbar-leading-button'),
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: _browseToSelectAnimation,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildAppBarTitle(bool isSelecting) {
|
||||
if (isSelecting) {
|
||||
return Selector<Selection<FilterGridItem<T>>, int>(
|
||||
selector: (context, selection) => selection.selection.length,
|
||||
builder: (context, count, child) => Text(context.l10n.collectionSelectionPageTitle(count)),
|
||||
);
|
||||
} else {
|
||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||
return InteractiveAppBarTitle(
|
||||
onTap: appMode.canSearch ? _goToSearch : null,
|
||||
child: SourceStateAwareAppBarTitle(
|
||||
title: Text(widget.title),
|
||||
source: source,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(AppMode appMode, Selection<FilterGridItem<T>> selection) {
|
||||
final selectedFilters = selection.selection.map((v) => v.filter).toSet();
|
||||
|
||||
PopupMenuItem<ChipSetAction> toMenuItem(ChipSetAction action, {bool enabled = true}) {
|
||||
return PopupMenuItem(
|
||||
value: action,
|
||||
enabled: enabled && actionDelegate.canApply(selectedFilters, action),
|
||||
child: MenuRow(
|
||||
text: action.getText(context),
|
||||
icon: action.getIcon(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void applyAction(ChipSetAction action) {
|
||||
actionDelegate.onActionSelected(context, selectedFilters, action);
|
||||
if (filterSelectionActions.contains(action)) {
|
||||
selection.browse();
|
||||
}
|
||||
}
|
||||
|
||||
final isSelecting = selection.isSelecting;
|
||||
final selectionRowActions = <ChipSetAction>[];
|
||||
|
||||
final buttonActions = <Widget>[];
|
||||
if (isSelecting) {
|
||||
final selectedFilters = selection.selection.map((v) => v.filter).toSet();
|
||||
final validActions = filterSelectionActions.where((action) => actionDelegate.isValid(selectedFilters, action)).toList();
|
||||
buttonActions.addAll(validActions.take(buttonActionCount).map(
|
||||
(action) {
|
||||
final enabled = actionDelegate.canApply(selectedFilters, action);
|
||||
return IconButton(
|
||||
icon: Icon(action.getIcon()),
|
||||
onPressed: enabled ? () => applyAction(action) : null,
|
||||
tooltip: action.getText(context),
|
||||
);
|
||||
},
|
||||
));
|
||||
selectionRowActions.addAll(validActions.skip(buttonActionCount));
|
||||
} else if (appMode.canSearch) {
|
||||
buttonActions.add(CollectionSearchButton(source: source));
|
||||
}
|
||||
|
||||
return [
|
||||
...buttonActions,
|
||||
PopupMenuButton<ChipSetAction>(
|
||||
key: const Key('appbar-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
final menuItems = <PopupMenuEntry<ChipSetAction>>[
|
||||
toMenuItem(ChipSetAction.sort),
|
||||
if (widget.groupable) toMenuItem(ChipSetAction.group),
|
||||
];
|
||||
|
||||
if (isSelecting) {
|
||||
final selectedItems = selection.selection;
|
||||
|
||||
if (selectionRowActions.isNotEmpty) {
|
||||
menuItems.add(const PopupMenuDivider());
|
||||
menuItems.addAll(selectionRowActions.map(toMenuItem));
|
||||
}
|
||||
|
||||
menuItems.addAll([
|
||||
const PopupMenuDivider(),
|
||||
toMenuItem(
|
||||
ChipSetAction.selectAll,
|
||||
enabled: selectedItems.length < actionDelegate.allItems.length,
|
||||
),
|
||||
toMenuItem(
|
||||
ChipSetAction.selectNone,
|
||||
enabled: selectedItems.isNotEmpty,
|
||||
),
|
||||
]);
|
||||
} else if (appMode == AppMode.main) {
|
||||
menuItems.addAll([
|
||||
toMenuItem(
|
||||
ChipSetAction.select,
|
||||
enabled: !widget.isEmpty,
|
||||
),
|
||||
toMenuItem(ChipSetAction.stats),
|
||||
]);
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
},
|
||||
onSelected: (action) {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => applyAction(action));
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _onActivityChange() {
|
||||
if (context.read<Selection<FilterGridItem<T>>>().isSelecting) {
|
||||
_browseToSelectAnimation.forward();
|
||||
} else {
|
||||
_browseToSelectAnimation.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void _goToSearch() {
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
source: source,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
import 'package:aves/model/actions/chip_actions.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/stats/stats.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
abstract class ChipSetActionDelegate {
|
||||
ChipSortFactor get sortFactor;
|
||||
|
||||
set sortFactor(ChipSortFactor factor);
|
||||
|
||||
void onActionSelected(BuildContext context, ChipSetAction action) {
|
||||
switch (action) {
|
||||
case ChipSetAction.sort:
|
||||
_showSortDialog(context);
|
||||
break;
|
||||
case ChipSetAction.stats:
|
||||
_goToStats(context);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showSortDialog(BuildContext context) async {
|
||||
final factor = await showDialog<ChipSortFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
|
||||
initialValue: sortFactor,
|
||||
options: {
|
||||
ChipSortFactor.date: context.l10n.chipSortDate,
|
||||
ChipSortFactor.name: context.l10n.chipSortName,
|
||||
ChipSortFactor.count: context.l10n.chipSortCount,
|
||||
},
|
||||
title: context.l10n.chipSortTitle,
|
||||
),
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
void _goToStats(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: StatsPage.routeName),
|
||||
builder: (context) => StatsPage(
|
||||
source: source,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumChipSetActionDelegate extends ChipSetActionDelegate {
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.albumSortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor;
|
||||
|
||||
@override
|
||||
void onActionSelected(BuildContext context, ChipSetAction action) {
|
||||
switch (action) {
|
||||
case ChipSetAction.group:
|
||||
_showGroupDialog(context);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.onActionSelected(context, action);
|
||||
}
|
||||
|
||||
Future<void> _showGroupDialog(BuildContext context) async {
|
||||
final factor = await showDialog<AlbumChipGroupFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<AlbumChipGroupFactor>(
|
||||
initialValue: settings.albumGroupFactor,
|
||||
options: {
|
||||
AlbumChipGroupFactor.importance: context.l10n.albumGroupTier,
|
||||
AlbumChipGroupFactor.volume: context.l10n.albumGroupVolume,
|
||||
AlbumChipGroupFactor.none: context.l10n.albumGroupNone,
|
||||
},
|
||||
title: context.l10n.albumGroupTitle,
|
||||
),
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CountryChipSetActionDelegate extends ChipSetActionDelegate {
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.countrySortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor;
|
||||
}
|
||||
|
||||
class TagChipSetActionDelegate extends ChipSetActionDelegate {
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.tagSortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor;
|
||||
}
|
|
@ -16,29 +16,25 @@ import 'package:aves/utils/constants.dart';
|
|||
import 'package:aves/widgets/collection/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/overlay.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DecoratedFilterChip extends StatelessWidget {
|
||||
final CollectionFilter filter;
|
||||
class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||
final T filter;
|
||||
final double extent, thumbnailExtent;
|
||||
final AvesEntry? coverEntry;
|
||||
final bool pinned, highlightable;
|
||||
final bool pinned;
|
||||
final FilterCallback? onTap;
|
||||
final OffsetFilterCallback? onLongPress;
|
||||
|
||||
const DecoratedFilterChip({
|
||||
const CoveredFilterChip({
|
||||
Key? key,
|
||||
required this.filter,
|
||||
required this.extent,
|
||||
double? thumbnailExtent,
|
||||
this.coverEntry,
|
||||
this.pinned = false,
|
||||
this.highlightable = true,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
}) : thumbnailExtent = thumbnailExtent ?? extent,
|
||||
super(key: key);
|
||||
|
||||
|
@ -89,41 +85,23 @@ class DecoratedFilterChip extends StatelessWidget {
|
|||
extent: thumbnailExtent,
|
||||
);
|
||||
final titlePadding = min<double>(4.0, extent / 32);
|
||||
final borderRadius = BorderRadius.all(radius(extent));
|
||||
Widget child = AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
background: backgroundImage,
|
||||
details: _buildDetails(source, filter),
|
||||
borderRadius: borderRadius,
|
||||
padding: titlePadding,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
|
||||
child = Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
child,
|
||||
if (highlightable)
|
||||
ChipHighlightOverlay(
|
||||
filter: filter,
|
||||
extent: extent,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
child = SizedBox(
|
||||
return SizedBox(
|
||||
width: extent,
|
||||
height: extent,
|
||||
child: child,
|
||||
child: AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
background: backgroundImage,
|
||||
details: _buildDetails(source, filter),
|
||||
borderRadius: BorderRadius.all(radius(extent)),
|
||||
padding: titlePadding,
|
||||
onTap: onTap,
|
||||
onLongPress: null,
|
||||
),
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildDetails(CollectionSource source, CollectionFilter filter) {
|
||||
Widget _buildDetails(CollectionSource source, T filter) {
|
||||
final padding = min<double>(8.0, extent / 16);
|
||||
final iconSize = min<double>(14.0, extent / 8);
|
||||
final fontSize = min<double>(14.0, extent / 6);
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/widgets/common/grid/overlay.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/overlay.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FilterChipGridDecorator<T extends CollectionFilter, U extends FilterGridItem<T>> extends StatelessWidget {
|
||||
final U gridItem;
|
||||
final double extent;
|
||||
final Widget child;
|
||||
|
||||
const FilterChipGridDecorator({
|
||||
Key? key,
|
||||
required this.gridItem,
|
||||
required this.extent,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderRadius = BorderRadius.all(CoveredFilterChip.radius(extent));
|
||||
return SizedBox(
|
||||
width: extent,
|
||||
height: extent,
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
child,
|
||||
GridItemSelectionOverlay<FilterGridItem<T>>(
|
||||
item: gridItem,
|
||||
borderRadius: borderRadius,
|
||||
padding: EdgeInsets.all(extent / 24),
|
||||
),
|
||||
ChipHighlightOverlay(
|
||||
filter: gridItem.filter,
|
||||
extent: extent,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
|
@ -13,7 +15,9 @@ 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/section_layout.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/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
|
@ -21,8 +25,9 @@ import 'package:aves/widgets/common/providers/tile_extent_controller_provider.da
|
|||
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/decorated_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_chip_grid_decorator.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';
|
||||
|
@ -38,28 +43,27 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
final String? settingsRouteKey;
|
||||
final Widget appBar;
|
||||
final double appBarHeight;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool showHeaders;
|
||||
final bool showHeaders, selectable;
|
||||
final ValueNotifier<String> queryNotifier;
|
||||
final QueryTest<T>? applyQuery;
|
||||
final Widget Function() emptyBuilder;
|
||||
final FilterCallback onTap;
|
||||
final OffsetFilterCallback? onLongPress;
|
||||
|
||||
const FilterGridPage({
|
||||
Key? key,
|
||||
this.settingsRouteKey,
|
||||
required this.appBar,
|
||||
this.appBarHeight = kToolbarHeight,
|
||||
required this.filterSections,
|
||||
required this.sections,
|
||||
required this.sortFactor,
|
||||
required this.showHeaders,
|
||||
required this.selectable,
|
||||
required this.queryNotifier,
|
||||
this.applyQuery,
|
||||
required this.emptyBuilder,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
}) : super(key: key);
|
||||
|
||||
static const Color detailColor = Color(0xFFE0E0E0);
|
||||
|
@ -68,24 +72,34 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: DoubleBackPopScope(
|
||||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: AnimatedBuilder(
|
||||
animation: covers,
|
||||
builder: (context, child) => FilterGrid<T>(
|
||||
settingsRouteKey: settingsRouteKey,
|
||||
appBar: appBar,
|
||||
appBarHeight: appBarHeight,
|
||||
filterSections: filterSections,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
queryNotifier: queryNotifier,
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: emptyBuilder,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
body: WillPopScope(
|
||||
onWillPop: () {
|
||||
final selection = context.read<Selection<FilterGridItem<T>>>();
|
||||
if (selection.isSelecting) {
|
||||
selection.browse();
|
||||
return SynchronousFuture(false);
|
||||
}
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: DoubleBackPopScope(
|
||||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: AnimatedBuilder(
|
||||
animation: covers,
|
||||
builder: (context, child) => FilterGrid<T>(
|
||||
settingsRouteKey: settingsRouteKey,
|
||||
appBar: appBar,
|
||||
appBarHeight: appBarHeight,
|
||||
sections: sections,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
selectable: selectable,
|
||||
queryNotifier: queryNotifier,
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: emptyBuilder,
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -102,28 +116,27 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
|||
final String? settingsRouteKey;
|
||||
final Widget appBar;
|
||||
final double appBarHeight;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool showHeaders;
|
||||
final bool showHeaders, selectable;
|
||||
final ValueNotifier<String> queryNotifier;
|
||||
final QueryTest<T>? applyQuery;
|
||||
final Widget Function() emptyBuilder;
|
||||
final FilterCallback onTap;
|
||||
final OffsetFilterCallback? onLongPress;
|
||||
|
||||
const FilterGrid({
|
||||
Key? key,
|
||||
required this.settingsRouteKey,
|
||||
required this.appBar,
|
||||
required this.appBarHeight,
|
||||
required this.filterSections,
|
||||
required this.sections,
|
||||
required this.sortFactor,
|
||||
required this.showHeaders,
|
||||
required this.selectable,
|
||||
required this.queryNotifier,
|
||||
required this.applyQuery,
|
||||
required this.emptyBuilder,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -152,14 +165,14 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
|
|||
child: _FilterGridContent<T>(
|
||||
appBar: widget.appBar,
|
||||
appBarHeight: widget.appBarHeight,
|
||||
filterSections: widget.filterSections,
|
||||
sections: widget.sections,
|
||||
sortFactor: widget.sortFactor,
|
||||
showHeaders: widget.showHeaders,
|
||||
selectable: widget.selectable,
|
||||
queryNotifier: widget.queryNotifier,
|
||||
applyQuery: widget.applyQuery,
|
||||
emptyBuilder: widget.emptyBuilder,
|
||||
onTap: widget.onTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -167,14 +180,13 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
|
|||
|
||||
class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||
final Widget appBar;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool showHeaders;
|
||||
final bool showHeaders, selectable;
|
||||
final ValueNotifier<String> queryNotifier;
|
||||
final Widget Function() emptyBuilder;
|
||||
final QueryTest<T>? applyQuery;
|
||||
final FilterCallback onTap;
|
||||
final OffsetFilterCallback? onLongPress;
|
||||
|
||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||
|
||||
|
@ -182,14 +194,14 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
Key? key,
|
||||
required this.appBar,
|
||||
required double appBarHeight,
|
||||
required this.filterSections,
|
||||
required this.sections,
|
||||
required this.sortFactor,
|
||||
required this.showHeaders,
|
||||
required this.selectable,
|
||||
required this.queryNotifier,
|
||||
required this.applyQuery,
|
||||
required this.emptyBuilder,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
}) : super(key: key) {
|
||||
_appBarHeightNotifier.value = appBarHeight;
|
||||
}
|
||||
|
@ -199,15 +211,15 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
return ValueListenableBuilder<String>(
|
||||
valueListenable: queryNotifier,
|
||||
builder: (context, query, child) {
|
||||
Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
||||
Map<ChipSectionKey, List<FilterGridItem<T>>> visibleSections;
|
||||
if (applyQuery == null) {
|
||||
visibleFilterSections = filterSections;
|
||||
visibleSections = sections;
|
||||
} else {
|
||||
visibleFilterSections = {};
|
||||
filterSections.forEach((sectionKey, sectionFilters) {
|
||||
visibleSections = {};
|
||||
sections.forEach((sectionKey, sectionFilters) {
|
||||
final visibleFilters = applyQuery!(sectionFilters, query);
|
||||
if (visibleFilters.isNotEmpty) {
|
||||
visibleFilterSections[sectionKey] = visibleFilters.toList();
|
||||
visibleSections[sectionKey] = visibleFilters.toList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -216,48 +228,54 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||
builder: (context, tileExtent, child) {
|
||||
return Selector<TileExtentController, Tuple3<double, int, double>>(
|
||||
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
|
||||
builder: (context, c, child) {
|
||||
final scrollableWidth = c.item1;
|
||||
final columnCount = c.item2;
|
||||
final tileSpacing = c.item3;
|
||||
// do not listen for animation delay change
|
||||
final controller = Provider.of<TileExtentController>(context, listen: false);
|
||||
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
|
||||
return SectionedFilterListLayoutProvider<T>(
|
||||
sections: visibleFilterSections,
|
||||
showHeaders: showHeaders,
|
||||
scrollableWidth: scrollableWidth,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
tileExtent: tileExtent,
|
||||
tileBuilder: (gridItem) {
|
||||
final filter = gridItem.filter;
|
||||
final entry = gridItem.entry;
|
||||
return MetaData(
|
||||
metaData: ScalerMetadata(FilterGridItem<T>(filter, entry)),
|
||||
child: DecoratedFilterChip(
|
||||
key: Key(filter.key),
|
||||
filter: filter,
|
||||
extent: tileExtent,
|
||||
pinned: pinnedFilters.contains(filter),
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
);
|
||||
},
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: _FilterSectionedContent<T>(
|
||||
appBar: appBar,
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
visibleFilterSections: visibleFilterSections,
|
||||
sortFactor: sortFactor,
|
||||
emptyBuilder: emptyBuilder,
|
||||
scrollController: PrimaryScrollController.of(context)!,
|
||||
),
|
||||
);
|
||||
});
|
||||
return GridTheme(
|
||||
extent: tileExtent,
|
||||
child: Selector<TileExtentController, Tuple3<double, int, double>>(
|
||||
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
|
||||
builder: (context, c, child) {
|
||||
final scrollableWidth = c.item1;
|
||||
final columnCount = c.item2;
|
||||
final tileSpacing = c.item3;
|
||||
// do not listen for animation delay change
|
||||
final controller = Provider.of<TileExtentController>(context, listen: false);
|
||||
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
|
||||
return SectionedFilterListLayoutProvider<T>(
|
||||
sections: visibleSections,
|
||||
showHeaders: showHeaders,
|
||||
scrollableWidth: scrollableWidth,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
tileExtent: tileExtent,
|
||||
tileBuilder: (gridItem) {
|
||||
final filter = gridItem.filter;
|
||||
return MetaData(
|
||||
metaData: ScalerMetadata(gridItem),
|
||||
child: FilterChipGridDecorator<T, FilterGridItem<T>>(
|
||||
gridItem: gridItem,
|
||||
extent: tileExtent,
|
||||
child: CoveredFilterChip(
|
||||
key: Key(filter.key),
|
||||
filter: filter,
|
||||
extent: tileExtent,
|
||||
pinned: pinnedFilters.contains(filter),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: _FilterSectionedContent<T>(
|
||||
appBar: appBar,
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
visibleSections: visibleSections,
|
||||
sortFactor: sortFactor,
|
||||
selectable: selectable,
|
||||
emptyBuilder: emptyBuilder,
|
||||
scrollController: PrimaryScrollController.of(context)!,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
return sectionedListLayoutProvider;
|
||||
|
@ -269,16 +287,18 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget {
|
||||
final Widget appBar;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> visibleSections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool selectable;
|
||||
final Widget Function() emptyBuilder;
|
||||
final ScrollController scrollController;
|
||||
|
||||
const _FilterSectionedContent({
|
||||
required this.appBar,
|
||||
required this.appBarHeightNotifier,
|
||||
required this.visibleFilterSections,
|
||||
required this.visibleSections,
|
||||
required this.sortFactor,
|
||||
required this.selectable,
|
||||
required this.emptyBuilder,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
@ -293,7 +313,7 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
@override
|
||||
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
|
||||
|
||||
Map<ChipSectionKey, List<FilterGridItem<T>>> get visibleFilterSections => widget.visibleFilterSections;
|
||||
Map<ChipSectionKey, List<FilterGridItem<T>>> get visibleSections => widget.visibleSections;
|
||||
|
||||
Widget Function() get emptyBuilder => widget.emptyBuilder;
|
||||
|
||||
|
@ -328,14 +348,23 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
child: scrollView,
|
||||
);
|
||||
|
||||
return scaler;
|
||||
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
|
||||
final selector = GridSelectionGestureDetector<FilterGridItem<T>>(
|
||||
selectable: isMainMode && widget.selectable,
|
||||
items: visibleSections.values.expand((v) => v).toList(),
|
||||
scrollController: scrollController,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
child: scaler,
|
||||
);
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
Future<void> _checkInitHighlight() async {
|
||||
final highlightInfo = context.read<HighlightInfo>();
|
||||
final filter = highlightInfo.clear();
|
||||
if (filter is T) {
|
||||
final gridItem = visibleFilterSections.values.expand((list) => list).firstWhereOrNull((gridItem) => gridItem.filter == filter);
|
||||
final gridItem = visibleSections.values.expand((list) => list).firstWhereOrNull((gridItem) => gridItem.filter == filter);
|
||||
if (gridItem != null) {
|
||||
await Future.delayed(Durations.highlightScrollInitDelay);
|
||||
highlightInfo.trackItem(gridItem, highlightItem: filter);
|
||||
|
@ -367,19 +396,18 @@ class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
|||
extent: extent,
|
||||
spacing: tileSpacing,
|
||||
borderWidth: AvesFilterChip.outlineWidth,
|
||||
borderRadius: DecoratedFilterChip.radius(extent),
|
||||
borderRadius: CoveredFilterChip.radius(extent),
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
scaledBuilder: (item, extent) {
|
||||
final filter = item.filter;
|
||||
return DecoratedFilterChip(
|
||||
return CoveredFilterChip(
|
||||
filter: filter,
|
||||
extent: extent,
|
||||
thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
pinned: pinnedFilters.contains(filter),
|
||||
highlightable: false,
|
||||
);
|
||||
},
|
||||
highlightItem: (item) => item.filter,
|
||||
|
|
|
@ -1,38 +1,22 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/actions/chip_actions.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/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/common/app_bar_subtitle.dart';
|
||||
import 'package:aves/widgets/common/app_bar_title.dart';
|
||||
import 'package:aves/widgets/common/basic/menu_row.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
||||
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/app_bar.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||
import 'package:aves/widgets/search/search_button.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final String title;
|
||||
final ChipSetActionDelegate chipSetActionDelegate;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool groupable, showHeaders;
|
||||
final ChipActionDelegate chipActionDelegate;
|
||||
final List<ChipAction> Function(T filter) chipActionsBuilder;
|
||||
final ChipSetActionDelegate actionDelegate;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
final Widget Function() emptyBuilder;
|
||||
|
||||
|
@ -43,114 +27,54 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
required this.sortFactor,
|
||||
this.groupable = false,
|
||||
this.showHeaders = false,
|
||||
required this.chipSetActionDelegate,
|
||||
required this.chipActionDelegate,
|
||||
required this.chipActionsBuilder,
|
||||
required this.actionDelegate,
|
||||
required this.filterSections,
|
||||
required this.emptyBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
|
||||
return FilterGridPage<T>(
|
||||
key: const Key('filter-grid-page'),
|
||||
appBar: SliverAppBar(
|
||||
title: InteractiveAppBarTitle(
|
||||
onTap: () => _goToSearch(context),
|
||||
child: SourceStateAwareAppBarTitle(
|
||||
title: Text(title),
|
||||
return SelectionProvider<FilterGridItem<T>>(
|
||||
child: Builder(
|
||||
builder: (context) => FilterGridPage<T>(
|
||||
key: const Key('filter-grid-page'),
|
||||
appBar: FilterGridAppBar<T>(
|
||||
source: source,
|
||||
title: title,
|
||||
actionDelegate: actionDelegate,
|
||||
groupable: groupable,
|
||||
isEmpty: filterSections.isEmpty,
|
||||
),
|
||||
),
|
||||
actions: _buildActions(context),
|
||||
titleSpacing: 0,
|
||||
floating: true,
|
||||
),
|
||||
filterSections: filterSections,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
queryNotifier: ValueNotifier(''),
|
||||
emptyBuilder: () => ValueListenableBuilder<SourceState>(
|
||||
valueListenable: source.stateNotifier,
|
||||
builder: (context, sourceState, child) {
|
||||
return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
onTap: (filter) => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
collection: CollectionLens(
|
||||
source: source,
|
||||
filters: [filter],
|
||||
),
|
||||
sections: filterSections,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
selectable: true,
|
||||
queryNotifier: ValueNotifier(''),
|
||||
emptyBuilder: () => ValueListenableBuilder<SourceState>(
|
||||
valueListenable: source.stateNotifier,
|
||||
builder: (context, sourceState, child) {
|
||||
return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
onTap: (filter) => _goToCollection(context, filter),
|
||||
),
|
||||
),
|
||||
onLongPress: isMainMode ? _showMenu as OffsetFilterCallback : null,
|
||||
);
|
||||
}
|
||||
|
||||
void _showMenu(BuildContext context, T filter, Offset? tapPosition) async {
|
||||
final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox;
|
||||
const touchArea = Size(40, 40);
|
||||
final selectedAction = await showMenu<ChipAction>(
|
||||
context: context,
|
||||
position: RelativeRect.fromRect((tapPosition ?? Offset.zero) & touchArea, Offset.zero & overlay.size),
|
||||
items: chipActionsBuilder(filter)
|
||||
.map((action) => PopupMenuItem(
|
||||
value: action,
|
||||
child: MenuRow(text: action.getText(context), icon: action.getIcon()),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
if (selectedAction != null) {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipActionDelegate.onActionSelected(context, filter, selectedAction));
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
return [
|
||||
CollectionSearchButton(source: source),
|
||||
PopupMenuButton<ChipSetAction>(
|
||||
key: const Key('appbar-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
key: const Key('menu-sort'),
|
||||
value: ChipSetAction.sort,
|
||||
child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort),
|
||||
),
|
||||
if (groupable)
|
||||
PopupMenuItem(
|
||||
value: ChipSetAction.group,
|
||||
child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChipSetAction.stats,
|
||||
child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (action) {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipSetActionDelegate.onActionSelected(context, action));
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _goToSearch(BuildContext context) {
|
||||
void _goToCollection(BuildContext context, CollectionFilter filter) {
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
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) {
|
||||
|
@ -167,21 +91,23 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
return a.filter.compareTo(b.filter);
|
||||
}
|
||||
|
||||
static Iterable<FilterGridItem<T>> sort<T extends CollectionFilter>(ChipSortFactor sortFactor, CollectionSource source, Set<T> filters) {
|
||||
Iterable<FilterGridItem<T>> toGridItem(CollectionSource source, Set<T> filters) {
|
||||
return filters.map((filter) => FilterGridItem(
|
||||
filter,
|
||||
source.recentEntry(filter),
|
||||
));
|
||||
static List<FilterGridItem<T>> sort<T extends CollectionFilter>(ChipSortFactor sortFactor, CollectionSource source, Set<T> filters) {
|
||||
List<FilterGridItem<T>> toGridItem(CollectionSource source, Set<T> filters) {
|
||||
return filters
|
||||
.map((filter) => FilterGridItem(
|
||||
filter,
|
||||
source.recentEntry(filter),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Iterable<FilterGridItem<T>> allMapEntries = {};
|
||||
List<FilterGridItem<T>> allMapEntries = [];
|
||||
switch (sortFactor) {
|
||||
case ChipSortFactor.name:
|
||||
allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByName);
|
||||
allMapEntries = toGridItem(source, filters)..sort(compareFiltersByName);
|
||||
break;
|
||||
case ChipSortFactor.date:
|
||||
allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByDate);
|
||||
allMapEntries = toGridItem(source, filters)..sort(compareFiltersByDate);
|
||||
break;
|
||||
case ChipSortFactor.count:
|
||||
final filtersWithCount = List.of(filters.map((filter) => MapEntry(filter, source.count(filter))));
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:aves/widgets/common/grid/header.dart';
|
|||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FilterChipSectionHeader extends StatelessWidget {
|
||||
class FilterChipSectionHeader<T> extends StatelessWidget {
|
||||
final ChipSectionKey sectionKey;
|
||||
|
||||
const FilterChipSectionHeader({
|
||||
|
@ -12,11 +12,10 @@ class FilterChipSectionHeader extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionHeader(
|
||||
return SectionHeader<T>(
|
||||
sectionKey: sectionKey,
|
||||
leading: sectionKey.leading,
|
||||
title: sectionKey.title,
|
||||
selectable: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
|
|||
|
||||
@override
|
||||
Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent) {
|
||||
return FilterChipSectionHeader(
|
||||
return FilterChipSectionHeader<FilterGridItem<T>>(
|
||||
sectionKey: sectionKey as ChipSectionKey,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:aves/model/actions/chip_actions.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -8,8 +7,7 @@ import 'package:aves/model/source/location.dart';
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/country_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -35,36 +33,32 @@ class CountryListPage extends StatelessWidget {
|
|||
builder: (context, s, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<CountriesChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage<LocationFilter>(
|
||||
source: source,
|
||||
title: context.l10n.countryPageTitle,
|
||||
sortFactor: settings.countrySortFactor,
|
||||
chipSetActionDelegate: CountryChipSetActionDelegate(),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.setCover,
|
||||
ChipAction.hide,
|
||||
],
|
||||
filterSections: _getCountryEntries(source),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.location,
|
||||
text: context.l10n.countryEmpty,
|
||||
),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = _getGridItems(source);
|
||||
return FilterNavigationPage<LocationFilter>(
|
||||
source: source,
|
||||
title: context.l10n.countryPageTitle,
|
||||
sortFactor: settings.countrySortFactor,
|
||||
actionDelegate: CountryChipSetActionDelegate(gridItems),
|
||||
filterSections: _groupToSections(gridItems),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.location,
|
||||
text: context.l10n.countryEmpty,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _getCountryEntries(CollectionSource source) {
|
||||
List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) {
|
||||
final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)).toSet();
|
||||
|
||||
final sorted = FilterNavigationPage.sort(settings.countrySortFactor, source, filters);
|
||||
return _group(sorted);
|
||||
return FilterNavigationPage.sort(settings.countrySortFactor, source, filters);
|
||||
}
|
||||
|
||||
static Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _group(Iterable<FilterGridItem<LocationFilter>> sortedMapEntries) {
|
||||
static Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _groupToSections(Iterable<FilterGridItem<LocationFilter>> sortedMapEntries) {
|
||||
final pinned = settings.pinnedFilters.whereType<LocationFilter>();
|
||||
final byPin = groupBy<FilterGridItem<LocationFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter));
|
||||
final pinnedMapEntries = (byPin[true] ?? []);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:aves/model/actions/chip_actions.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -8,8 +7,7 @@ import 'package:aves/model/source/tag.dart';
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/tag_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -35,36 +33,32 @@ class TagListPage extends StatelessWidget {
|
|||
builder: (context, s, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage<TagFilter>(
|
||||
source: source,
|
||||
title: context.l10n.tagPageTitle,
|
||||
sortFactor: settings.tagSortFactor,
|
||||
chipSetActionDelegate: TagChipSetActionDelegate(),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.setCover,
|
||||
ChipAction.hide,
|
||||
],
|
||||
filterSections: _getTagEntries(source),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.tag,
|
||||
text: context.l10n.tagEmpty,
|
||||
),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = _getGridItems(source);
|
||||
return FilterNavigationPage<TagFilter>(
|
||||
source: source,
|
||||
title: context.l10n.tagPageTitle,
|
||||
sortFactor: settings.tagSortFactor,
|
||||
actionDelegate: TagChipSetActionDelegate(gridItems),
|
||||
filterSections: _groupToSections(gridItems),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.tag,
|
||||
text: context.l10n.tagEmpty,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _getTagEntries(CollectionSource source) {
|
||||
List<FilterGridItem<TagFilter>> _getGridItems(CollectionSource source) {
|
||||
final filters = source.sortedTags.map((tag) => TagFilter(tag)).toSet();
|
||||
|
||||
final sorted = FilterNavigationPage.sort(settings.tagSortFactor, source, filters);
|
||||
return _group(sorted);
|
||||
return FilterNavigationPage.sort(settings.tagSortFactor, source, filters);
|
||||
}
|
||||
|
||||
static Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _group(Iterable<FilterGridItem<TagFilter>> sortedMapEntries) {
|
||||
static Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _groupToSections(Iterable<FilterGridItem<TagFilter>> sortedMapEntries) {
|
||||
final pinned = settings.pinnedFilters.whereType<TagFilter>();
|
||||
final byPin = groupBy<FilterGridItem<TagFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter));
|
||||
final pinnedMapEntries = (byPin[true] ?? []);
|
||||
|
|
|
@ -26,12 +26,13 @@ class CollectionSearchButton extends StatelessWidget {
|
|||
|
||||
void _goToSearch(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
source: source,
|
||||
parentCollection: parentCollection,
|
||||
),
|
||||
));
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
source: source,
|
||||
parentCollection: parentCollection,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ class HiddenFilterPage extends StatelessWidget {
|
|||
.map((filter) => AvesFilterChip(
|
||||
filter: filter,
|
||||
removable: true,
|
||||
onTap: (filter) => context.read<CollectionSource>().changeFilterVisibility(filter, true),
|
||||
onTap: (filter) => context.read<CollectionSource>().changeFilterVisibility({filter}, true),
|
||||
onLongPress: null,
|
||||
))
|
||||
.toList(),
|
||||
|
|
|
@ -36,7 +36,7 @@ class VideoSection extends StatelessWidget {
|
|||
if (!standalonePage)
|
||||
SwitchListTile(
|
||||
value: currentShowVideos,
|
||||
onChanged: (v) => context.read<CollectionSource>().changeFilterVisibility(MimeFilter.video, v),
|
||||
onChanged: (v) => context.read<CollectionSource>().changeFilterVisibility({MimeFilter.video}, v),
|
||||
title: Text(context.l10n.settingsVideoShowVideos),
|
||||
),
|
||||
const VideoActionsTile(),
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:math';
|
|||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/theme.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -91,7 +91,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
final horizontalMargin = SizedBox(width: marginWidth);
|
||||
const separator = SizedBox(width: separatorWidth);
|
||||
|
||||
return ThumbnailTheme(
|
||||
return GridTheme(
|
||||
extent: extent,
|
||||
showLocation: false,
|
||||
child: StreamBuilder<MultiPageInfo?>(
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:aves/utils/pedantic.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/panorama_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:aves/utils/pedantic.dart';
|
||||
|
||||
class PanoramaOverlay extends StatelessWidget {
|
||||
final AvesEntry entry;
|
||||
|
|
|
@ -130,7 +130,7 @@ void selectFirstAlbum() {
|
|||
|
||||
await driver.tap(find.descendant(
|
||||
of: find.byValueKey('filter-grid-page'),
|
||||
matching: find.byType('DecoratedFilterChip'),
|
||||
matching: find.byType('CoveredFilterChip'),
|
||||
firstMatchOnly: true,
|
||||
));
|
||||
await driver.waitUntilNoTransientCallbacks();
|
||||
|
|
Loading…
Reference in a new issue