reorganized filter grid page files
This commit is contained in:
parent
c2b353ed3b
commit
b61b6a057e
10 changed files with 436 additions and 405 deletions
|
@ -8,8 +8,8 @@ import 'package:aves/utils/durations.dart';
|
|||
import 'package:aves/widgets/album/filter_bar.dart';
|
||||
import 'package:aves/widgets/album/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/collection_group_dialog.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/collection_sort_dialog.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart';
|
||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
||||
import 'package:aves/widgets/common/entry_actions.dart';
|
||||
|
|
|
@ -17,7 +17,9 @@ import 'package:aves/widgets/album/collection_page.dart';
|
|||
import 'package:aves/widgets/common/aves_logo.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/debug_page.dart';
|
||||
import 'package:aves/widgets/filter_grid_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/settings/settings_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -17,7 +17,7 @@ import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
|||
import 'package:aves/widgets/common/dialog.dart';
|
||||
import 'package:aves/widgets/common/entry_actions.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grid_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
|
|
@ -1,401 +0,0 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings.dart';
|
||||
import 'package:aves/model/source/album.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/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/raster.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/vector.dart';
|
||||
import 'package:aves/widgets/app_drawer.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/chip_sort_dialog.dart';
|
||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/menu_row.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AlbumListPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
|
||||
const AlbumListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, ChipSortFactor>(
|
||||
selector: (context, s) => s.albumSortFactor,
|
||||
builder: (context, albumSortFactor, child) {
|
||||
return AnimatedBuilder(
|
||||
animation: androidFileUtils.appNameChangeNotifier,
|
||||
builder: (context, child) => StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
return FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Albums',
|
||||
actions: _buildActions(),
|
||||
filterEntries: _getAlbumEntries(),
|
||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: 'No albums',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, ImageEntry> _getAlbumEntries() {
|
||||
final entriesByDate = source.sortedEntriesForFilterList;
|
||||
final albumEntries = source.sortedAlbums.map((album) {
|
||||
return MapEntry(
|
||||
album,
|
||||
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
switch (settings.albumSortFactor) {
|
||||
case ChipSortFactor.date:
|
||||
albumEntries.sort((a, b) {
|
||||
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
|
||||
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
||||
});
|
||||
return Map.fromEntries(albumEntries);
|
||||
case ChipSortFactor.name:
|
||||
default:
|
||||
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
||||
for (var album in source.sortedAlbums) {
|
||||
switch (androidFileUtils.getAlbumType(album)) {
|
||||
case AlbumType.regular:
|
||||
regularAlbums.add(album);
|
||||
break;
|
||||
case AlbumType.app:
|
||||
appAlbums.add(album);
|
||||
break;
|
||||
default:
|
||||
specialAlbums.add(album);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) {
|
||||
return MapEntry(
|
||||
album,
|
||||
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildActions() {
|
||||
return [
|
||||
Builder(
|
||||
builder: (context) => PopupMenuButton<ChipAction>(
|
||||
key: Key('appbar-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
key: Key('menu-sort'),
|
||||
value: ChipAction.sort,
|
||||
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (action) => _onChipActionSelected(context, action),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _onChipActionSelected(BuildContext context, ChipAction action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||
switch (action) {
|
||||
case ChipAction.sort:
|
||||
final factor = await showDialog<ChipSortFactor>(
|
||||
context: context,
|
||||
builder: (context) => ChipSortDialog(initialValue: settings.albumSortFactor),
|
||||
);
|
||||
if (factor != null) {
|
||||
settings.albumSortFactor = factor;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
class CountryListPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
|
||||
const CountryListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<LocationsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Countries',
|
||||
filterEntries: source.getCountryEntries(),
|
||||
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.location,
|
||||
text: 'No countries',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TagListPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
|
||||
const TagListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Tags',
|
||||
filterEntries: source.getTagEntries(),
|
||||
filterBuilder: (s) => TagFilter(s),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.tag,
|
||||
text: 'No tags',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FilterNavigationPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final String title;
|
||||
final List<Widget> actions;
|
||||
final Map<String, ImageEntry> filterEntries;
|
||||
final CollectionFilter Function(String key) filterBuilder;
|
||||
final Widget Function() emptyBuilder;
|
||||
|
||||
const FilterNavigationPage({
|
||||
@required this.source,
|
||||
@required this.title,
|
||||
this.actions,
|
||||
@required this.filterEntries,
|
||||
@required this.filterBuilder,
|
||||
@required this.emptyBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FilterGridPage(
|
||||
source: source,
|
||||
appBar: SliverAppBar(
|
||||
title: SourceStateAwareAppBarTitle(
|
||||
title: Text(title),
|
||||
source: source,
|
||||
),
|
||||
actions: actions,
|
||||
floating: true,
|
||||
),
|
||||
filterEntries: filterEntries,
|
||||
filterBuilder: filterBuilder,
|
||||
emptyBuilder: () => ValueListenableBuilder<SourceState>(
|
||||
valueListenable: source.stateNotifier,
|
||||
builder: (context, sourceState, child) {
|
||||
return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
onPressed: (filter) => Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CollectionPage(CollectionLens(
|
||||
source: source,
|
||||
filters: [filter],
|
||||
groupFactor: settings.collectionGroupFactor,
|
||||
sortFactor: settings.collectionSortFactor,
|
||||
)),
|
||||
),
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FilterGridPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final Widget appBar;
|
||||
final Map<String, ImageEntry> filterEntries;
|
||||
final CollectionFilter Function(String key) filterBuilder;
|
||||
final Widget Function() emptyBuilder;
|
||||
final FilterCallback onPressed;
|
||||
|
||||
const FilterGridPage({
|
||||
@required this.source,
|
||||
@required this.appBar,
|
||||
@required this.filterEntries,
|
||||
@required this.filterBuilder,
|
||||
@required this.emptyBuilder,
|
||||
@required this.onPressed,
|
||||
});
|
||||
|
||||
List<String> get filterKeys => filterEntries.keys.toList();
|
||||
|
||||
static const Color detailColor = Color(0xFFE0E0E0);
|
||||
static const double maxCrossAxisExtent = 180;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.size.width,
|
||||
builder: (c, mqWidth, child) {
|
||||
final columnCount = (mqWidth / maxCrossAxisExtent).ceil();
|
||||
return AnimationLimiter(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
appBar,
|
||||
filterKeys.isEmpty
|
||||
? SliverFillRemaining(
|
||||
child: emptyBuilder(),
|
||||
hasScrollBody: false,
|
||||
)
|
||||
: SliverPadding(
|
||||
padding: EdgeInsets.all(AvesFilterChip.outlineWidth),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, i) {
|
||||
final key = filterKeys[i];
|
||||
final child = DecoratedFilterChip(
|
||||
source: source,
|
||||
filter: filterBuilder(key),
|
||||
entry: filterEntries[key],
|
||||
onPressed: onPressed,
|
||||
);
|
||||
return AnimationConfiguration.staggeredGrid(
|
||||
position: i,
|
||||
columnCount: columnCount,
|
||||
duration: Durations.staggeredAnimation,
|
||||
delay: Durations.staggeredAnimationDelay,
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: filterKeys.length,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.viewInsets.bottom,
|
||||
builder: (context, mqViewInsetsBottom, child) {
|
||||
return SizedBox(height: mqViewInsetsBottom);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
drawer: AppDrawer(
|
||||
source: source,
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DecoratedFilterChip extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final CollectionFilter filter;
|
||||
final ImageEntry entry;
|
||||
final FilterCallback onPressed;
|
||||
|
||||
const DecoratedFilterChip({
|
||||
@required this.source,
|
||||
@required this.filter,
|
||||
@required this.entry,
|
||||
@required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget backgroundImage;
|
||||
if (entry != null) {
|
||||
backgroundImage = entry.isSvg
|
||||
? ThumbnailVectorImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
)
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
);
|
||||
}
|
||||
return AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
background: backgroundImage,
|
||||
details: _buildDetails(filter),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetails(CollectionFilter filter) {
|
||||
final count = Text(
|
||||
'${source.count(filter)}',
|
||||
style: TextStyle(color: FilterGridPage.detailColor),
|
||||
);
|
||||
return filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
AIcons.removableStorage,
|
||||
size: 16,
|
||||
color: FilterGridPage.detailColor,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
count,
|
||||
],
|
||||
)
|
||||
: count;
|
||||
}
|
||||
}
|
||||
|
||||
enum ChipAction {
|
||||
sort,
|
||||
}
|
128
lib/widgets/filter_grids/albums_page.dart
Normal file
128
lib/widgets/filter_grids/albums_page.dart
Normal file
|
@ -0,0 +1,128 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/chip_sort_dialog.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/menu_row.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AlbumListPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
|
||||
const AlbumListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, ChipSortFactor>(
|
||||
selector: (context, s) => s.albumSortFactor,
|
||||
builder: (context, albumSortFactor, child) {
|
||||
return AnimatedBuilder(
|
||||
animation: androidFileUtils.appNameChangeNotifier,
|
||||
builder: (context, child) => StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
return FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Albums',
|
||||
actions: _buildActions(),
|
||||
filterEntries: _getAlbumEntries(),
|
||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: 'No albums',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, ImageEntry> _getAlbumEntries() {
|
||||
final entriesByDate = source.sortedEntriesForFilterList;
|
||||
final albumEntries = source.sortedAlbums.map((album) {
|
||||
return MapEntry(
|
||||
album,
|
||||
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
switch (settings.albumSortFactor) {
|
||||
case ChipSortFactor.date:
|
||||
albumEntries.sort((a, b) {
|
||||
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
|
||||
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
||||
});
|
||||
return Map.fromEntries(albumEntries);
|
||||
case ChipSortFactor.name:
|
||||
default:
|
||||
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
||||
for (var album in source.sortedAlbums) {
|
||||
switch (androidFileUtils.getAlbumType(album)) {
|
||||
case AlbumType.regular:
|
||||
regularAlbums.add(album);
|
||||
break;
|
||||
case AlbumType.app:
|
||||
appAlbums.add(album);
|
||||
break;
|
||||
default:
|
||||
specialAlbums.add(album);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) {
|
||||
return MapEntry(
|
||||
album,
|
||||
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildActions() {
|
||||
return [
|
||||
Builder(
|
||||
builder: (context) => PopupMenuButton<ChipAction>(
|
||||
key: Key('appbar-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
key: Key('menu-sort'),
|
||||
value: ChipAction.sort,
|
||||
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (action) => _onChipActionSelected(context, action),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _onChipActionSelected(BuildContext context, ChipAction action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||
switch (action) {
|
||||
case ChipAction.sort:
|
||||
final factor = await showDialog<ChipSortFactor>(
|
||||
context: context,
|
||||
builder: (context) => ChipSortDialog(initialValue: settings.albumSortFactor),
|
||||
);
|
||||
if (factor != null) {
|
||||
settings.albumSortFactor = factor;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
30
lib/widgets/filter_grids/countries_page.dart
Normal file
30
lib/widgets/filter_grids/countries_page.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CountryListPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
|
||||
const CountryListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<LocationsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Countries',
|
||||
filterEntries: source.getCountryEntries(),
|
||||
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.location,
|
||||
text: 'No countries',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
71
lib/widgets/filter_grids/decorated_filter_chip.dart
Normal file
71
lib/widgets/filter_grids/decorated_filter_chip.dart
Normal file
|
@ -0,0 +1,71 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/raster.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/vector.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DecoratedFilterChip extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final CollectionFilter filter;
|
||||
final ImageEntry entry;
|
||||
final FilterCallback onPressed;
|
||||
|
||||
const DecoratedFilterChip({
|
||||
@required this.source,
|
||||
@required this.filter,
|
||||
@required this.entry,
|
||||
@required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget backgroundImage;
|
||||
if (entry != null) {
|
||||
backgroundImage = entry.isSvg
|
||||
? ThumbnailVectorImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
)
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
);
|
||||
}
|
||||
return AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
background: backgroundImage,
|
||||
details: _buildDetails(filter),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetails(CollectionFilter filter) {
|
||||
final count = Text(
|
||||
'${source.count(filter)}',
|
||||
style: TextStyle(color: FilterGridPage.detailColor),
|
||||
);
|
||||
return filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
AIcons.removableStorage,
|
||||
size: 16,
|
||||
color: FilterGridPage.detailColor,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
count,
|
||||
],
|
||||
)
|
||||
: count;
|
||||
}
|
||||
}
|
171
lib/widgets/filter_grids/filter_grid_page.dart
Normal file
171
lib/widgets/filter_grids/filter_grid_page.dart
Normal file
|
@ -0,0 +1,171 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/app_drawer.dart';
|
||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilterNavigationPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final String title;
|
||||
final List<Widget> actions;
|
||||
final Map<String, ImageEntry> filterEntries;
|
||||
final CollectionFilter Function(String key) filterBuilder;
|
||||
final Widget Function() emptyBuilder;
|
||||
|
||||
const FilterNavigationPage({
|
||||
@required this.source,
|
||||
@required this.title,
|
||||
this.actions,
|
||||
@required this.filterEntries,
|
||||
@required this.filterBuilder,
|
||||
@required this.emptyBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FilterGridPage(
|
||||
source: source,
|
||||
appBar: SliverAppBar(
|
||||
title: SourceStateAwareAppBarTitle(
|
||||
title: Text(title),
|
||||
source: source,
|
||||
),
|
||||
actions: actions,
|
||||
floating: true,
|
||||
),
|
||||
filterEntries: filterEntries,
|
||||
filterBuilder: filterBuilder,
|
||||
emptyBuilder: () => ValueListenableBuilder<SourceState>(
|
||||
valueListenable: source.stateNotifier,
|
||||
builder: (context, sourceState, child) {
|
||||
return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
onPressed: (filter) => Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CollectionPage(CollectionLens(
|
||||
source: source,
|
||||
filters: [filter],
|
||||
groupFactor: settings.collectionGroupFactor,
|
||||
sortFactor: settings.collectionSortFactor,
|
||||
)),
|
||||
),
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FilterGridPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final Widget appBar;
|
||||
final Map<String, ImageEntry> filterEntries;
|
||||
final CollectionFilter Function(String key) filterBuilder;
|
||||
final Widget Function() emptyBuilder;
|
||||
final FilterCallback onPressed;
|
||||
|
||||
const FilterGridPage({
|
||||
@required this.source,
|
||||
@required this.appBar,
|
||||
@required this.filterEntries,
|
||||
@required this.filterBuilder,
|
||||
@required this.emptyBuilder,
|
||||
@required this.onPressed,
|
||||
});
|
||||
|
||||
List<String> get filterKeys => filterEntries.keys.toList();
|
||||
|
||||
static const Color detailColor = Color(0xFFE0E0E0);
|
||||
static const double maxCrossAxisExtent = 180;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.size.width,
|
||||
builder: (c, mqWidth, child) {
|
||||
final columnCount = (mqWidth / maxCrossAxisExtent).ceil();
|
||||
return AnimationLimiter(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
appBar,
|
||||
filterKeys.isEmpty
|
||||
? SliverFillRemaining(
|
||||
child: emptyBuilder(),
|
||||
hasScrollBody: false,
|
||||
)
|
||||
: SliverPadding(
|
||||
padding: EdgeInsets.all(AvesFilterChip.outlineWidth),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, i) {
|
||||
final key = filterKeys[i];
|
||||
final child = DecoratedFilterChip(
|
||||
source: source,
|
||||
filter: filterBuilder(key),
|
||||
entry: filterEntries[key],
|
||||
onPressed: onPressed,
|
||||
);
|
||||
return AnimationConfiguration.staggeredGrid(
|
||||
position: i,
|
||||
columnCount: columnCount,
|
||||
duration: Durations.staggeredAnimation,
|
||||
delay: Durations.staggeredAnimationDelay,
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: filterKeys.length,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.viewInsets.bottom,
|
||||
builder: (context, mqViewInsetsBottom, child) {
|
||||
return SizedBox(height: mqViewInsetsBottom);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
drawer: AppDrawer(
|
||||
source: source,
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ChipAction {
|
||||
sort,
|
||||
}
|
30
lib/widgets/filter_grids/tags_page.dart
Normal file
30
lib/widgets/filter_grids/tags_page.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TagListPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
|
||||
const TagListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Tags',
|
||||
filterEntries: source.getTagEntries(),
|
||||
filterBuilder: (s) => TagFilter(s),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.tag,
|
||||
text: 'No tags',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import 'package:aves/utils/android_file_utils.dart';
|
|||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grid_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
Loading…
Reference in a new issue