search: revert custom app bar, added matching suggestions
This commit is contained in:
parent
6bcb89db85
commit
fc014a6274
16 changed files with 159 additions and 90 deletions
|
@ -5,7 +5,6 @@ import 'package:aves/model/settings.dart';
|
|||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/viewer_service.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -75,13 +75,16 @@ class CollectionLens with ChangeNotifier {
|
|||
Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}';
|
||||
|
||||
void addFilter(CollectionFilter filter) {
|
||||
if (filters.contains(filter)) return;
|
||||
if (filter == null || filters.contains(filter)) return;
|
||||
if (filter.isUnique) {
|
||||
filters.removeWhere((old) => old.typeKey == filter.typeKey);
|
||||
}
|
||||
filters.add(filter);
|
||||
onFilterChanged();
|
||||
}
|
||||
|
||||
void removeFilter(CollectionFilter filter) {
|
||||
if (!filters.contains(filter)) return;
|
||||
if (filter == null || !filters.contains(filter)) return;
|
||||
filters.remove(filter);
|
||||
onFilterChanged();
|
||||
}
|
||||
|
|
|
@ -16,13 +16,15 @@ class AlbumFilter extends CollectionFilter {
|
|||
|
||||
final String album;
|
||||
|
||||
const AlbumFilter(this.album);
|
||||
final String uniqueName;
|
||||
|
||||
const AlbumFilter(this.album, this.uniqueName);
|
||||
|
||||
@override
|
||||
bool filter(ImageEntry entry) => entry.directory == album;
|
||||
|
||||
@override
|
||||
String get label => album.split(separator).last;
|
||||
String get label => uniqueName ?? album.split(separator).last;
|
||||
|
||||
@override
|
||||
String get tooltip => album;
|
||||
|
|
|
@ -26,6 +26,8 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
|||
|
||||
bool filter(ImageEntry entry);
|
||||
|
||||
bool get isUnique => true;
|
||||
|
||||
String get label;
|
||||
|
||||
String get tooltip => label;
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class QueryFilter extends CollectionFilter {
|
||||
static const type = 'query';
|
||||
|
@ -21,6 +13,9 @@ class QueryFilter extends CollectionFilter {
|
|||
@override
|
||||
bool filter(ImageEntry entry) => entry.search(query);
|
||||
|
||||
@override
|
||||
bool get isUnique => false;
|
||||
|
||||
@override
|
||||
String get label => '${query}';
|
||||
|
||||
|
|
|
@ -13,6 +13,9 @@ class TagFilter extends CollectionFilter {
|
|||
@override
|
||||
bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag);
|
||||
|
||||
@override
|
||||
bool get isUnique => false;
|
||||
|
||||
@override
|
||||
String get label => tag;
|
||||
|
||||
|
|
|
@ -6,6 +6,19 @@ class Constants {
|
|||
// so we give it a `strutStyle` with a slightly larger height
|
||||
static const overflowStrutStyle = StrutStyle(height: 1.3);
|
||||
|
||||
static const titleTextStyle = TextStyle(
|
||||
color: Color(0xFFEEEEEE),
|
||||
fontSize: 20,
|
||||
fontFamily: 'Concourse Caps',
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(0, 2),
|
||||
blurRadius: 3,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling
|
||||
static const double thumbnailCacheExtent = 50;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:aves/model/filters/query.dart';
|
|||
import 'package:aves/model/settings.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/album/filter_bar.dart';
|
||||
import 'package:aves/widgets/album/search_delegate.dart';
|
||||
import 'package:aves/widgets/common/menu_row.dart';
|
||||
import 'package:aves/widgets/stats.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -121,9 +122,17 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
builder: (context) {
|
||||
switch (stateNotifier.value) {
|
||||
case PageState.browse:
|
||||
return IconButton(
|
||||
icon: Icon(OMIcons.search),
|
||||
onPressed: () => stateNotifier.value = PageState.search,
|
||||
return Consumer<CollectionLens>(
|
||||
builder: (context, collection, child) => IconButton(
|
||||
icon: Icon(OMIcons.search),
|
||||
onPressed: () async {
|
||||
final filter = await showSearch(
|
||||
context: context,
|
||||
delegate: ImageSearchDelegate(collection),
|
||||
);
|
||||
collection.addFilter(filter);
|
||||
},
|
||||
),
|
||||
);
|
||||
case PageState.search:
|
||||
return IconButton(
|
||||
|
|
|
@ -98,7 +98,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
|||
leading: IconUtils.getAlbumIcon(context: context, album: album),
|
||||
title: CollectionSource.getUniqueAlbumName(album, source.sortedAlbums),
|
||||
dense: true,
|
||||
filter: AlbumFilter(album),
|
||||
filter: AlbumFilter(album, CollectionSource.getUniqueAlbumName(album, source.sortedAlbums)),
|
||||
);
|
||||
final buildTagEntry = (tag) => _FilteredCollectionNavTile(
|
||||
source: source,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/widgets/album/collection_app_bar.dart';
|
||||
import 'package:aves/widgets/album/collection_drawer.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/album/thumbnail_collection.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -47,6 +48,7 @@ class CollectionPageBody extends StatelessWidget {
|
|||
appBar: CollectionAppBar(
|
||||
stateNotifier: _stateNotifier,
|
||||
),
|
||||
emptyBuilder: (context) => EmptyContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
31
lib/widgets/album/empty.dart
Normal file
31
lib/widgets/album/empty.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||
|
||||
class EmptyContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const color = Color(0xFF607D8B);
|
||||
return Align(
|
||||
alignment: const FractionalOffset(.5, .4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(
|
||||
OMIcons.photo,
|
||||
size: 64,
|
||||
color: color,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Nothing!',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 22,
|
||||
fontFamily: 'Concourse',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,9 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilterBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8);
|
||||
|
||||
static final double preferredHeight = kMinInteractiveDimension + padding.vertical;
|
||||
static final double preferredHeight = kMinInteractiveDimension;
|
||||
|
||||
@override
|
||||
final Size preferredSize = Size.fromHeight(preferredHeight);
|
||||
|
@ -20,7 +18,6 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
|
|||
// specify transparent as a workaround to prevent
|
||||
// chip border clipping when the floating app bar is fading
|
||||
color: Colors.transparent,
|
||||
padding: padding,
|
||||
height: preferredSize.height,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
// cancel notification bubbling so that the draggable scrollbar
|
||||
|
@ -28,9 +25,8 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
|
|||
onNotification: (notification) => true,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
primary: false,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2),
|
||||
padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2) + const EdgeInsets.symmetric(horizontal: 6),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= filters.length) return null;
|
||||
final filter = filters[index];
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/model/collection_source.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/country.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/gif.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/album/thumbnail_collection.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/video.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
|
||||
class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
||||
final CollectionLens collection;
|
||||
|
||||
ImageSearchDelegate(this.collection);
|
||||
|
@ -46,56 +52,70 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
|
|||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
if (query.isEmpty) {
|
||||
showSuggestions(context);
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return MediaQueryDataProvider(
|
||||
child: ChangeNotifierProvider<CollectionLens>.value(
|
||||
value: CollectionLens(
|
||||
source: collection.source,
|
||||
filters: [QueryFilter(query.toLowerCase())],
|
||||
groupFactor: collection.groupFactor,
|
||||
sortFactor: collection.sortFactor,
|
||||
),
|
||||
child: ThumbnailCollection(
|
||||
emptyBuilder: (context) => _EmptyContent(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const color = Color(0xFF607D8B);
|
||||
return Align(
|
||||
alignment: const FractionalOffset(.5, .4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(
|
||||
OMIcons.photo,
|
||||
size: 64,
|
||||
color: color,
|
||||
final source = collection.source;
|
||||
final upQuery = query.toUpperCase();
|
||||
final containQuery = (String s) => s.toUpperCase().contains(upQuery);
|
||||
return SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
..._buildFilterRow(
|
||||
filters: [FavouriteFilter(), VideoFilter(), GifFilter()].where((f) => containQuery(f.label)),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No match',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 22,
|
||||
fontFamily: 'Concourse',
|
||||
),
|
||||
..._buildFilterRow(
|
||||
title: 'Countries',
|
||||
filters: source.sortedCountries.where(containQuery).map((s) => CountryFilter(s)),
|
||||
),
|
||||
..._buildFilterRow(
|
||||
title: 'Albums',
|
||||
filters: source.sortedAlbums.where(containQuery).map((s) => AlbumFilter(s, CollectionSource.getUniqueAlbumName(s, source.sortedAlbums))).where((f) => containQuery(f.uniqueName)),
|
||||
),
|
||||
..._buildFilterRow(
|
||||
title: 'Tags',
|
||||
filters: source.sortedTags.where(containQuery).map((s) => TagFilter(s)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildFilterRow({String title, @required Iterable<CollectionFilter> filters}) {
|
||||
if (filters.isEmpty) return [];
|
||||
final filtersList = filters.toList();
|
||||
return [
|
||||
if (title != null && title.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
title,
|
||||
style: Constants.titleTextStyle,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: kMinInteractiveDimension,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2) + const EdgeInsets.symmetric(horizontal: 6),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= filtersList.length) return null;
|
||||
final filter = filtersList[index];
|
||||
return Center(
|
||||
child: AvesFilterChip(
|
||||
filter: filter,
|
||||
onPressed: (filter) => close(context, filter),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
itemCount: filtersList.length,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
close(context, QueryFilter(query));
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/utils/time_utils.dart';
|
||||
import 'package:aves/widgets/common/fx/outlined_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -56,18 +57,6 @@ class TitleSectionHeader extends StatelessWidget {
|
|||
|
||||
static const leadingDimension = 32.0;
|
||||
static const leadingPadding = EdgeInsets.only(right: 8, bottom: 4);
|
||||
static const textStyle = TextStyle(
|
||||
color: Color(0xFFEEEEEE),
|
||||
fontSize: 20,
|
||||
fontFamily: 'Concourse Caps',
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(0, 2),
|
||||
blurRadius: 3,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -83,7 +72,7 @@ class TitleSectionHeader extends StatelessWidget {
|
|||
)
|
||||
: null,
|
||||
text: title,
|
||||
style: textStyle,
|
||||
style: Constants.titleTextStyle,
|
||||
outlineWidth: 2,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/model/collection_source.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/gif.dart';
|
||||
|
@ -13,11 +15,13 @@ import 'package:intl/intl.dart';
|
|||
|
||||
class BasicSection extends StatelessWidget {
|
||||
final ImageEntry entry;
|
||||
final CollectionLens collection;
|
||||
final FilterCallback onFilter;
|
||||
|
||||
const BasicSection({
|
||||
Key key,
|
||||
@required this.entry,
|
||||
this.collection,
|
||||
@required this.onFilter,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -44,11 +48,12 @@ class BasicSection extends StatelessWidget {
|
|||
ValueListenableBuilder(
|
||||
valueListenable: entry.isFavouriteNotifier,
|
||||
builder: (context, isFavourite, child) {
|
||||
final album = entry.directory;
|
||||
final filters = [
|
||||
if (entry.isVideo) VideoFilter(),
|
||||
if (entry.isGif) GifFilter(),
|
||||
if (isFavourite) FavouriteFilter(),
|
||||
if (entry.directory != null) AlbumFilter(entry.directory),
|
||||
if (album != null) AlbumFilter(album, CollectionSource.getUniqueAlbumName(album, collection?.source?.sortedAlbums)),
|
||||
...tags.map((tag) => TagFilter(tag)),
|
||||
]..sort();
|
||||
if (filters.isEmpty) return const SizedBox.shrink();
|
||||
|
|
|
@ -82,7 +82,7 @@ class InfoPageState extends State<InfoPage> {
|
|||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: BasicSection(entry: entry, onFilter: _goToFilteredCollection)),
|
||||
Expanded(child: BasicSection(entry: entry, collection: collection, onFilter: _goToFilteredCollection)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: locationSection),
|
||||
],
|
||||
|
@ -91,7 +91,7 @@ class InfoPageState extends State<InfoPage> {
|
|||
: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
[
|
||||
BasicSection(entry: entry, onFilter: _goToFilteredCollection),
|
||||
BasicSection(entry: entry, collection: collection, onFilter: _goToFilteredCollection),
|
||||
locationSection,
|
||||
],
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue