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 {
|
class CollectionSource {
|
||||||
final List<ImageEntry> _rawEntries;
|
final List<ImageEntry> _rawEntries;
|
||||||
final Set<String> _folderPaths = {};
|
final Set<String> _folderPaths = {};
|
||||||
|
final Map<CollectionFilter, int> _filterEntryCountMap = {};
|
||||||
final EventBus _eventBus = EventBus();
|
final EventBus _eventBus = EventBus();
|
||||||
|
|
||||||
List<String> sortedAlbums = List.unmodifiable([]);
|
List<String> sortedAlbums = List.unmodifiable([]);
|
||||||
|
@ -117,12 +118,14 @@ class CollectionSource {
|
||||||
return compareAsciiUpperCase(ua, ub);
|
return compareAsciiUpperCase(ua, ub);
|
||||||
});
|
});
|
||||||
sortedAlbums = List.unmodifiable(sorted);
|
sortedAlbums = List.unmodifiable(sorted);
|
||||||
|
_filterEntryCountMap.clear();
|
||||||
eventBus.fire(AlbumsChangedEvent());
|
eventBus.fire(AlbumsChangedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateTags() {
|
void updateTags() {
|
||||||
final tags = _rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase);
|
final tags = _rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase);
|
||||||
sortedTags = List.unmodifiable(tags);
|
sortedTags = List.unmodifiable(tags);
|
||||||
|
_filterEntryCountMap.clear();
|
||||||
eventBus.fire(TagsChangedEvent());
|
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));
|
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}');
|
sortedCountries = lister((address) => '${address.countryName};${address.countryCode}');
|
||||||
sortedPlaces = lister((address) => address.place);
|
sortedPlaces = lister((address) => address.place);
|
||||||
|
_filterEntryCountMap.clear();
|
||||||
eventBus.fire(LocationsChangedEvent());
|
eventBus.fire(LocationsChangedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +145,7 @@ class CollectionSource {
|
||||||
});
|
});
|
||||||
_rawEntries.addAll(entries);
|
_rawEntries.addAll(entries);
|
||||||
_folderPaths.addAll(_rawEntries.map((entry) => entry.directory).toSet());
|
_folderPaths.addAll(_rawEntries.map((entry) => entry.directory).toSet());
|
||||||
|
_filterEntryCountMap.clear();
|
||||||
eventBus.fire(const EntryAddedEvent());
|
eventBus.fire(const EntryAddedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,10 +153,12 @@ class CollectionSource {
|
||||||
entries.forEach((entry) => entry.removeFromFavourites());
|
entries.forEach((entry) => entry.removeFromFavourites());
|
||||||
_rawEntries.removeWhere(entries.contains);
|
_rawEntries.removeWhere(entries.contains);
|
||||||
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
|
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
|
||||||
|
_filterEntryCountMap.clear();
|
||||||
eventBus.fire(EntryRemovedEvent(entries));
|
eventBus.fire(EntryRemovedEvent(entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
void notifyMovedEntries(Iterable<ImageEntry> movedEntries) {
|
void notifyMovedEntries(Iterable<ImageEntry> entries) {
|
||||||
|
_filterEntryCountMap.clear();
|
||||||
eventBus.fire(EntryMovedEvent(entries));
|
eventBus.fire(EntryMovedEvent(entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,9 +232,8 @@ class CollectionSource {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO TLAD cache counts, invalidate them on any add/remove
|
|
||||||
int count(CollectionFilter filter) {
|
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 {
|
Future _moveSelection(BuildContext context, {@required bool copy}) async {
|
||||||
|
final source = collection.source;
|
||||||
|
var isNewAlbum = false;
|
||||||
final destinationAlbum = await Navigator.push(
|
final destinationAlbum = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute<String>(
|
MaterialPageRoute<String>(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final source = collection.source;
|
|
||||||
return FilterGridPage(
|
return FilterGridPage(
|
||||||
source: source,
|
source: source,
|
||||||
appBar: SliverAppBar(
|
appBar: SliverAppBar(
|
||||||
|
@ -74,6 +75,7 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
builder: (context) => CreateAlbumDialog(),
|
builder: (context) => CreateAlbumDialog(),
|
||||||
);
|
);
|
||||||
if (newAlbum != null && newAlbum.isNotEmpty) {
|
if (newAlbum != null && newAlbum.isNotEmpty) {
|
||||||
|
isNewAlbum = true;
|
||||||
Navigator.pop<String>(context, newAlbum);
|
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')}');
|
_showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '${count} item', other: '${count} items')}');
|
||||||
}
|
}
|
||||||
if (movedCount > 0) {
|
if (movedCount > 0) {
|
||||||
final source = collection.source;
|
|
||||||
if (copy) {
|
if (copy) {
|
||||||
final newEntries = movedOps.map((movedOp) {
|
final newEntries = movedOps.map((movedOp) {
|
||||||
final sourceUri = movedOp.uri;
|
final sourceUri = movedOp.uri;
|
||||||
|
@ -150,6 +151,9 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
source.cleanEmptyAlbums(fromAlbums);
|
source.cleanEmptyAlbums(fromAlbums);
|
||||||
source.notifyMovedEntries(movedEntries);
|
source.notifyMovedEntries(movedEntries);
|
||||||
}
|
}
|
||||||
|
if (isNewAlbum) {
|
||||||
|
source.updateAlbums();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
collection.clearSelection();
|
collection.clearSelection();
|
||||||
collection.browse();
|
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_lens.dart';
|
||||||
import 'package:aves/model/collection_source.dart';
|
import 'package:aves/model/collection_source.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.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/android_file_utils.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
import 'package:aves/widgets/app_drawer.dart';
|
import 'package:aves/widgets/app_drawer.dart';
|
||||||
import 'package:aves/widgets/common/aves_filter_chip.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/icons.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class FilterNavigationPage extends StatelessWidget {
|
class FilterNavigationPage extends StatelessWidget {
|
||||||
|
@ -72,6 +76,7 @@ class FilterGridPage extends StatelessWidget {
|
||||||
List<String> get filterKeys => filterEntries.keys.toList();
|
List<String> get filterKeys => filterEntries.keys.toList();
|
||||||
|
|
||||||
static const Color detailColor = Color(0xFFE0E0E0);
|
static const Color detailColor = Color(0xFFE0E0E0);
|
||||||
|
static const double maxCrossAxisExtent = 180;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -87,34 +92,17 @@ class FilterGridPage extends StatelessWidget {
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, i) {
|
(context, i) {
|
||||||
final key = filterKeys[i];
|
final key = filterKeys[i];
|
||||||
final entry = filterEntries[key];
|
return DecoratedFilterChip(
|
||||||
Decoration decoration;
|
source: source,
|
||||||
// TODO TLAD add decoration for SVG
|
filter: filterBuilder(key),
|
||||||
if (entry != null && !entry.isSvg) {
|
entry: filterEntries[key],
|
||||||
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),
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: filterKeys.length,
|
childCount: filterKeys.length,
|
||||||
),
|
),
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 120,
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
crossAxisSpacing: 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) {
|
Widget _buildDetails(CollectionFilter filter) {
|
||||||
final count = Text(
|
final count = Text(
|
||||||
|
|
Loading…
Reference in a new issue