albums: svg decoration, update source for new albums

This commit is contained in:
Thibault Deckers 2020-06-02 11:24:02 +09:00
parent b9bf51ff83
commit ccb9482221
3 changed files with 129 additions and 28 deletions

View file

@ -12,6 +12,7 @@ import 'package:path/path.dart';
class CollectionSource {
final List<ImageEntry> _rawEntries;
final Set<String> _folderPaths = {};
final Map<CollectionFilter, int> _filterEntryCountMap = {};
final EventBus _eventBus = EventBus();
List<String> 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<String>.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<ImageEntry> movedEntries) {
void notifyMovedEntries(Iterable<ImageEntry> 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);
}
}

View file

@ -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<String>(
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<String>(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();

View file

@ -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<String> 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<DecoratedFilterChip> {
CollectionSource get source => widget.source;
CollectionFilter get filter => widget.filter;
ImageEntry get entry => widget.entry;
Future<Uint8List> _svgByteLoader;
@override
void initState() {
super.initState();
_svgByteLoader = _initSvgByteLoader();
}
@override
void didUpdateWidget(DecoratedFilterChip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != entry) {
_svgByteLoader = _initSvgByteLoader();
}
}
Future<Uint8List> _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<Uint8List> 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(