From ccb9482221da0a0672ad7cbb0b58692f78d00342 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 2 Jun 2020 11:24:02 +0900 Subject: [PATCH] albums: svg decoration, update source for new albums --- lib/model/collection_source.dart | 12 +- .../selection_action_delegate.dart | 8 +- lib/widgets/filter_grid_page.dart | 137 +++++++++++++++--- 3 files changed, 129 insertions(+), 28 deletions(-) diff --git a/lib/model/collection_source.dart b/lib/model/collection_source.dart index 219fd5054..60ca731a2 100644 --- a/lib/model/collection_source.dart +++ b/lib/model/collection_source.dart @@ -12,6 +12,7 @@ import 'package:path/path.dart'; class CollectionSource { final List _rawEntries; final Set _folderPaths = {}; + final Map _filterEntryCountMap = {}; final EventBus _eventBus = EventBus(); List sortedAlbums = List.unmodifiable([]); @@ -117,12 +118,14 @@ class CollectionSource { return compareAsciiUpperCase(ua, ub); }); sortedAlbums = List.unmodifiable(sorted); + _filterEntryCountMap.clear(); eventBus.fire(AlbumsChangedEvent()); } void updateTags() { final tags = _rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); sortedTags = List.unmodifiable(tags); + _filterEntryCountMap.clear(); eventBus.fire(TagsChangedEvent()); } @@ -131,6 +134,7 @@ class CollectionSource { final lister = (String Function(AddressDetails a) f) => List.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase)); sortedCountries = lister((address) => '${address.countryName};${address.countryCode}'); sortedPlaces = lister((address) => address.place); + _filterEntryCountMap.clear(); eventBus.fire(LocationsChangedEvent()); } @@ -141,6 +145,7 @@ class CollectionSource { }); _rawEntries.addAll(entries); _folderPaths.addAll(_rawEntries.map((entry) => entry.directory).toSet()); + _filterEntryCountMap.clear(); eventBus.fire(const EntryAddedEvent()); } @@ -148,10 +153,12 @@ class CollectionSource { entries.forEach((entry) => entry.removeFromFavourites()); _rawEntries.removeWhere(entries.contains); cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet()); + _filterEntryCountMap.clear(); eventBus.fire(EntryRemovedEvent(entries)); } - void notifyMovedEntries(Iterable movedEntries) { + void notifyMovedEntries(Iterable entries) { + _filterEntryCountMap.clear(); eventBus.fire(EntryMovedEvent(entries)); } @@ -225,9 +232,8 @@ class CollectionSource { ))); } - // TODO TLAD cache counts, invalidate them on any add/remove int count(CollectionFilter filter) { - return _rawEntries.where((entry) => filter.filter(entry)).length; + return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); } } diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 61f68ace7..d02dd9f5a 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -55,11 +55,12 @@ class SelectionActionDelegate with PermissionAwareMixin { } Future _moveSelection(BuildContext context, {@required bool copy}) async { + final source = collection.source; + var isNewAlbum = false; final destinationAlbum = await Navigator.push( context, MaterialPageRoute( builder: (context) { - final source = collection.source; return FilterGridPage( source: source, appBar: SliverAppBar( @@ -74,6 +75,7 @@ class SelectionActionDelegate with PermissionAwareMixin { builder: (context) => CreateAlbumDialog(), ); if (newAlbum != null && newAlbum.isNotEmpty) { + isNewAlbum = true; Navigator.pop(context, newAlbum); } }, @@ -111,7 +113,6 @@ class SelectionActionDelegate with PermissionAwareMixin { _showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '${count} item', other: '${count} items')}'); } if (movedCount > 0) { - final source = collection.source; if (copy) { final newEntries = movedOps.map((movedOp) { final sourceUri = movedOp.uri; @@ -150,6 +151,9 @@ class SelectionActionDelegate with PermissionAwareMixin { source.cleanEmptyAlbums(fromAlbums); source.notifyMovedEntries(movedEntries); } + if (isNewAlbum) { + source.updateAlbums(); + } } collection.clearSelection(); collection.browse(); diff --git a/lib/widgets/filter_grid_page.dart b/lib/widgets/filter_grid_page.dart index 99b68519b..d30ed273f 100644 --- a/lib/widgets/filter_grid_page.dart +++ b/lib/widgets/filter_grid_page.dart @@ -1,11 +1,14 @@ +import 'dart:typed_data'; +import 'dart:ui'; + 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/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/services/image_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/app_drawer.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; @@ -13,6 +16,7 @@ import 'package:aves/widgets/common/data_providers/media_query_data_provider.dar import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class FilterNavigationPage extends StatelessWidget { @@ -72,6 +76,7 @@ class FilterGridPage extends StatelessWidget { List get filterKeys => filterEntries.keys.toList(); static const Color detailColor = Color(0xFFE0E0E0); + static const double maxCrossAxisExtent = 180; @override Widget build(BuildContext context) { @@ -87,34 +92,17 @@ class FilterGridPage extends StatelessWidget { delegate: SliverChildBuilderDelegate( (context, i) { final key = filterKeys[i]; - final entry = filterEntries[key]; - Decoration decoration; - // TODO TLAD add decoration for SVG - if (entry != null && !entry.isSvg) { - decoration = BoxDecoration( - image: DecorationImage( - image: ThumbnailProvider( - entry: entry, - extent: Constants.thumbnailCacheExtent, - ), - fit: BoxFit.cover, - ), - borderRadius: AvesFilterChip.borderRadius, - ); - } - final filter = filterBuilder(key); - return AvesFilterChip( - filter: filter, - showGenericIcon: false, - decoration: decoration, - details: _buildDetails(filter), + return DecoratedFilterChip( + source: source, + filter: filterBuilder(key), + entry: filterEntries[key], onPressed: onPressed, ); }, childCount: filterKeys.length, ), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 120, + maxCrossAxisExtent: maxCrossAxisExtent, mainAxisSpacing: 8, crossAxisSpacing: 8, ), @@ -138,6 +126,109 @@ class FilterGridPage extends StatelessWidget { ), ); } +} + +class DecoratedFilterChip extends StatefulWidget { + 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 + _DecoratedFilterChipState createState() => _DecoratedFilterChipState(); +} + +class _DecoratedFilterChipState extends State { + CollectionSource get source => widget.source; + + CollectionFilter get filter => widget.filter; + + ImageEntry get entry => widget.entry; + + Future _svgByteLoader; + + @override + void initState() { + super.initState(); + _svgByteLoader = _initSvgByteLoader(); + } + + @override + void didUpdateWidget(DecoratedFilterChip oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.entry != entry) { + _svgByteLoader = _initSvgByteLoader(); + } + } + + Future _initSvgByteLoader() async { + if (entry == null || !entry.isSvg) return null; + + final uri = entry.uri; + final bytes = await ImageFileService.getImage(uri, entry.mimeType); + if (bytes == null || bytes.isEmpty) return bytes; + + final svgRoot = await svg.fromSvgBytes(bytes, uri); + const extent = FilterGridPage.maxCrossAxisExtent; + final picture = svgRoot.toPicture(size: const Size(extent, extent)); + final uiImage = await picture.toImage(extent.ceil(), extent.ceil()); + final data = await uiImage.toByteData(format: ImageByteFormat.png); + return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + } + + @override + Widget build(BuildContext context) { + if (entry == null || !entry.isSvg) { + Decoration decoration; + if (entry != null) { + decoration = BoxDecoration( + image: DecorationImage( + image: ThumbnailProvider( + entry: entry, + extent: FilterGridPage.maxCrossAxisExtent, + ), + fit: BoxFit.cover, + ), + borderRadius: AvesFilterChip.borderRadius, + ); + } + return _buildChip(decoration); + } + + return FutureBuilder( + future: _svgByteLoader, + builder: (context, AsyncSnapshot snapshot) { + Decoration decoration; + if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) { + decoration = BoxDecoration( + image: DecorationImage( + image: MemoryImage(snapshot.data), + fit: BoxFit.cover, + ), + borderRadius: AvesFilterChip.borderRadius, + ); + } + return _buildChip(decoration); + }, + ); + } + + AvesFilterChip _buildChip(Decoration decoration) { + return AvesFilterChip( + filter: filter, + showGenericIcon: false, + decoration: decoration, + details: _buildDetails(filter), + onPressed: widget.onPressed, + ); + } Widget _buildDetails(CollectionFilter filter) { final count = Text(