albums: svg decoration, update source for new albums
This commit is contained in:
parent
b9bf51ff83
commit
ccb9482221
3 changed files with 129 additions and 28 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue