info: added album filter chip

This commit is contained in:
Thibault Deckers 2020-03-26 18:16:21 +09:00
parent 246e697d9d
commit 0199f9bd22
11 changed files with 166 additions and 159 deletions

View file

@ -107,9 +107,7 @@ class _HomePageState extends State<HomePage> {
) )
: MediaStoreCollectionProvider( : MediaStoreCollectionProvider(
child: Consumer<CollectionLens>( child: Consumer<CollectionLens>(
builder: (context, collection, child) => CollectionPage( builder: (context, collection, child) => CollectionPage(collection),
collection: collection,
),
), ),
); );
}), }),

View file

@ -32,25 +32,6 @@ class CollectionLens with ChangeNotifier {
onEntryAdded(); onEntryAdded();
} }
factory CollectionLens.empty() {
return CollectionLens(
source: CollectionSource(),
);
}
factory CollectionLens.from(CollectionLens lens, CollectionFilter filter) {
if (lens == null) return null;
return CollectionLens(
source: lens.source,
filters: [
...lens.filters,
if (filter != null) filter,
],
groupFactor: lens.groupFactor,
sortFactor: lens.sortFactor,
);
}
@override @override
void dispose() { void dispose() {
_subscriptions _subscriptions
@ -60,8 +41,28 @@ class CollectionLens with ChangeNotifier {
super.dispose(); super.dispose();
} }
factory CollectionLens.empty() {
return CollectionLens(
source: CollectionSource(),
);
}
CollectionLens derive(CollectionFilter filter) {
return CollectionLens(
source: source,
filters: [
...filters,
if (filter != null) filter,
],
groupFactor: groupFactor,
sortFactor: sortFactor,
);
}
bool get isEmpty => _filteredEntries.isEmpty; bool get isEmpty => _filteredEntries.isEmpty;
int get entryCount => _filteredEntries.length;
int get imageCount => _filteredEntries.where((entry) => !entry.isVideo).length; int get imageCount => _filteredEntries.where((entry) => !entry.isVideo).length;
int get videoCount => _filteredEntries.where((entry) => entry.isVideo).length; int get videoCount => _filteredEntries.where((entry) => entry.isVideo).length;

View file

@ -278,13 +278,10 @@ class _FilteredCollectionNavTile extends StatelessWidget {
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CollectionPage( builder: (context) => CollectionPage(CollectionLens(
collection: CollectionLens(
source: source, source: source,
filters: [filter], filters: [filter],
), )),
title: title,
),
), ),
(route) => false, (route) => false,
); );

View file

@ -12,13 +12,8 @@ import 'package:provider/provider.dart';
class CollectionPage extends StatelessWidget { class CollectionPage extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
final String title;
const CollectionPage({ const CollectionPage(this.collection);
Key key,
@required this.collection,
this.title,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -30,7 +25,7 @@ class CollectionPage extends StatelessWidget {
appBar: collection.filters.isEmpty appBar: collection.filters.isEmpty
? AllCollectionAppBar() ? AllCollectionAppBar()
: SliverAppBar( : SliverAppBar(
title: Text(title), title: const Text('Aves'),
actions: _buildActions(), actions: _buildActions(),
bottom: FilterBar(collection.filters), bottom: FilterBar(collection.filters),
floating: true, floating: true,

View file

@ -1,6 +1,5 @@
import 'package:aves/model/collection_filters.dart'; import 'package:aves/model/collection_filters.dart';
import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/fullscreen/info/navigation_button.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,7 +8,6 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
static const double maxChipWidth = 160;
static const EdgeInsets padding = EdgeInsets.only(left: 8, right: 8, bottom: 8); static const EdgeInsets padding = EdgeInsets.only(left: 8, right: 8, bottom: 8);
@override @override
@ -38,44 +36,13 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
controller: _scrollController, controller: _scrollController,
primary: false, primary: false,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
padding: EdgeInsets.all(NavigationButton.buttonBorderWidth / 2), padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= filters.length) return null; if (index >= filters.length) return null;
final filter = filters[index]; final filter = filters[index];
final icon = filter.icon; return AvesFilterChip.fromFilter(
final label = filter.label; filter,
return ConstrainedBox( onPressed: (filter) {},
constraints: const BoxConstraints(maxWidth: maxChipWidth),
child: Tooltip(
message: label,
child: OutlineButton(
onPressed: () {},
borderSide: BorderSide(
color: stringToColor(label),
width: NavigationButton.buttonBorderWidth,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(42),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon),
const SizedBox(width: 8),
],
Flexible(
child: Text(
label,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
),
],
),
),
),
); );
}, },
separatorBuilder: (context, index) => const SizedBox(width: 8), separatorBuilder: (context, index) => const SizedBox(width: 8),

View file

@ -0,0 +1,67 @@
import 'package:aves/model/collection_filters.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:flutter/material.dart';
typedef FilterCallback = void Function(CollectionFilter filter);
class AvesFilterChip extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback onPressed;
const AvesFilterChip({
this.icon,
@required this.label,
@required this.onPressed,
});
factory AvesFilterChip.fromFilter(
CollectionFilter filter, {
@required FilterCallback onPressed,
}) =>
AvesFilterChip(
icon: filter.icon,
label: filter.label,
onPressed: onPressed != null ? () => onPressed(filter) : null,
);
static const double buttonBorderWidth = 2;
static const double maxChipWidth = 160;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: maxChipWidth),
child: Tooltip(
message: label,
child: OutlineButton(
onPressed: onPressed,
borderSide: BorderSide(
color: stringToColor(label),
width: buttonBorderWidth,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(42),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon),
const SizedBox(width: 8),
],
Flexible(
child: Text(
label,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
),
],
),
),
),
);
}
}

View file

@ -1,15 +1,19 @@
import 'package:aves/model/collection_filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/file_utils.dart'; import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class BasicSection extends StatelessWidget { class BasicSection extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
final FilterCallback onFilter;
const BasicSection({ const BasicSection({
Key key, Key key,
@required this.entry, @required this.entry,
@required this.onFilter,
}) : super(key: key); }) : super(key: key);
@override @override
@ -19,7 +23,11 @@ class BasicSection extends StatelessWidget {
final showMegaPixels = !entry.isVideo && !entry.isGif && entry.megaPixels != null && entry.megaPixels > 0; final showMegaPixels = !entry.isVideo && !entry.isGif && entry.megaPixels != null && entry.megaPixels > 0;
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}'; final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
return InfoRowGroup({ final filter = entry.directory != null ? AlbumFilter(entry.directory) : null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoRowGroup({
'Title': entry.title ?? '?', 'Title': entry.title ?? '?',
'Date': dateText, 'Date': dateText,
if (entry.isVideo) ..._buildVideoRows(), if (entry.isVideo) ..._buildVideoRows(),
@ -27,7 +35,16 @@ class BasicSection extends StatelessWidget {
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?', 'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?',
'URI': entry.uri ?? '?', 'URI': entry.uri ?? '?',
if (entry.path != null) 'Path': entry.path, if (entry.path != null) 'Path': entry.path,
}); }),
if (filter != null) ...[
const SizedBox(height: 8),
AvesFilterChip.fromFilter(
filter,
onPressed: onFilter,
),
]
],
);
} }
Map<String, String> _buildVideoRows() { Map<String, String> _buildVideoRows() {

View file

@ -1,10 +1,12 @@
import 'package:aves/model/collection_filters.dart';
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/fullscreen/info/basic_section.dart'; import 'package:aves/widgets/fullscreen/info/basic_section.dart';
import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart';
import 'package:aves/widgets/fullscreen/info/metadata_section.dart'; import 'package:aves/widgets/fullscreen/info/metadata_section.dart';
import 'package:aves/widgets/fullscreen/info/navigation_button.dart';
import 'package:aves/widgets/fullscreen/info/xmp_section.dart'; import 'package:aves/widgets/fullscreen/info/xmp_section.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -74,13 +76,14 @@ class InfoPageState extends State<InfoPage> {
entry: entry, entry: entry,
showTitle: !locationAtTop, showTitle: !locationAtTop,
visibleNotifier: widget.visibleNotifier, visibleNotifier: widget.visibleNotifier,
onFilter: _goToFilteredCollection,
); );
final basicAndLocationSliver = locationAtTop final basicAndLocationSliver = locationAtTop
? SliverToBoxAdapter( ? SliverToBoxAdapter(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded(child: BasicSection(entry: entry)), Expanded(child: BasicSection(entry: entry, onFilter: _goToFilteredCollection)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: locationSection), Expanded(child: locationSection),
], ],
@ -89,7 +92,7 @@ class InfoPageState extends State<InfoPage> {
: SliverList( : SliverList(
delegate: SliverChildListDelegate.fixed( delegate: SliverChildListDelegate.fixed(
[ [
BasicSection(entry: entry), BasicSection(entry: entry, onFilter: _goToFilteredCollection),
locationSection, locationSection,
], ],
), ),
@ -97,6 +100,7 @@ class InfoPageState extends State<InfoPage> {
final tagSliver = XmpTagSectionSliver( final tagSliver = XmpTagSectionSliver(
collection: collection, collection: collection,
entry: entry, entry: entry,
onFilter: _goToFilteredCollection,
); );
final metadataSliver = MetadataSectionSliver( final metadataSliver = MetadataSectionSliver(
entry: entry, entry: entry,
@ -158,6 +162,16 @@ class InfoPageState extends State<InfoPage> {
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
} }
void _goToFilteredCollection(CollectionFilter filter) {
if (collection == null) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CollectionPage(collection.derive(filter)),
),
);
}
} }
class SectionRow extends StatelessWidget { class SectionRow extends StatelessWidget {
@ -171,7 +185,7 @@ class SectionRow extends StatelessWidget {
final buildDivider = () => const SizedBox( final buildDivider = () => const SizedBox(
width: dim, width: dim,
child: Divider( child: Divider(
thickness: NavigationButton.buttonBorderWidth, thickness: AvesFilterChip.buttonBorderWidth,
color: Colors.white70, color: Colors.white70,
), ),
); );

View file

@ -4,9 +4,8 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_app_service.dart'; import 'package:aves/utils/android_app_service.dart';
import 'package:aves/utils/geo_utils.dart'; import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/fullscreen/info/navigation_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
@ -16,6 +15,7 @@ class LocationSection extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final bool showTitle; final bool showTitle;
final ValueNotifier<bool> visibleNotifier; final ValueNotifier<bool> visibleNotifier;
final FilterCallback onFilter;
const LocationSection({ const LocationSection({
Key key, Key key,
@ -23,6 +23,7 @@ class LocationSection extends StatefulWidget {
@required this.entry, @required this.entry,
@required this.showTitle, @required this.showTitle,
@required this.visibleNotifier, @required this.visibleNotifier,
@required this.onFilter,
}) : super(key: key); }) : super(key: key);
@override @override
@ -78,7 +79,10 @@ class _LocationSectionState extends State<LocationSection> {
} else if (entry.hasGps) { } else if (entry.hasGps) {
location = toDMS(entry.latLng).join(', '); location = toDMS(entry.latLng).join(', ');
} }
final country = entry.addressDetails?.countryName ?? ''; final country = entry.addressDetails?.countryName;
final filters = [
if (country != null) CountryFilter(country),
];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -102,17 +106,17 @@ class _LocationSectionState extends State<LocationSection> {
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: InfoRowGroup({'Address': location}), child: InfoRowGroup({'Address': location}),
), ),
if (country.isNotEmpty) if (filters.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: NavigationButton.buttonBorderWidth / 2) + const EdgeInsets.only(top: 8), padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.buttonBorderWidth / 2) + const EdgeInsets.only(top: 8),
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
children: [ children: filters
NavigationButton( .map((filter) => AvesFilterChip.fromFilter(
label: country, filter,
onPressed: () => _goToCountry(context, country), onPressed: widget.onFilter,
), ))
], .toList(),
), ),
), ),
], ],
@ -124,19 +128,6 @@ class _LocationSectionState extends State<LocationSection> {
} }
void _handleChange() => setState(() {}); void _handleChange() => setState(() {});
void _goToCountry(BuildContext context, String country) {
if (collection == null) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CollectionPage(
collection: CollectionLens.from(collection, CountryFilter(country)),
title: country,
),
),
);
}
} }
class ImageMap extends StatefulWidget { class ImageMap extends StatefulWidget {

View file

@ -1,29 +0,0 @@
import 'package:aves/utils/color_utils.dart';
import 'package:flutter/material.dart';
class NavigationButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
const NavigationButton({
@required this.label,
@required this.onPressed,
});
static const double buttonBorderWidth = 2;
@override
Widget build(BuildContext context) {
return OutlineButton(
onPressed: onPressed,
borderSide: BorderSide(
color: stringToColor(label),
width: buttonBorderWidth,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(42),
),
child: Text(label),
);
}
}

View file

@ -1,9 +1,8 @@
import 'package:aves/model/collection_filters.dart'; import 'package:aves/model/collection_filters.dart';
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/fullscreen/info/navigation_button.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
@ -11,11 +10,13 @@ import 'package:outline_material_icons/outline_material_icons.dart';
class XmpTagSectionSliver extends AnimatedWidget { class XmpTagSectionSliver extends AnimatedWidget {
final CollectionLens collection; final CollectionLens collection;
final ImageEntry entry; final ImageEntry entry;
final FilterCallback onFilter;
XmpTagSectionSliver({ XmpTagSectionSliver({
Key key, Key key,
@required this.collection, @required this.collection,
@required this.entry, @required this.entry,
@required this.onFilter,
}) : super(key: key, listenable: entry.metadataChangeNotifier); }) : super(key: key, listenable: entry.metadataChangeNotifier);
@override @override
@ -28,13 +29,14 @@ class XmpTagSectionSliver extends AnimatedWidget {
: [ : [
const SectionRow(OMIcons.localOffer), const SectionRow(OMIcons.localOffer),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: NavigationButton.buttonBorderWidth / 2), padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.buttonBorderWidth / 2),
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
children: tags children: tags
.map((tag) => NavigationButton( .map((tag) => TagFilter(tag))
label: tag, .map((filter) => AvesFilterChip.fromFilter(
onPressed: () => _goToTag(context, tag), filter,
onPressed: onFilter,
)) ))
.toList(), .toList(),
), ),
@ -43,17 +45,4 @@ class XmpTagSectionSliver extends AnimatedWidget {
), ),
); );
} }
void _goToTag(BuildContext context, String tag) {
if (collection == null) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CollectionPage(
collection: CollectionLens.from(collection, TagFilter(tag)),
title: tag,
),
),
);
}
} }