From 499e71f9031bc87893c6b515a37181cf8b9b0448 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 27 Oct 2020 16:52:48 +0900 Subject: [PATCH] info: added staggered animation to metadata section --- lib/main.dart | 1 + lib/utils/durations.dart | 2 + lib/widgets/app_debug_page.dart | 2 +- .../image_providers/thumbnail_provider.dart | 3 +- lib/widgets/fullscreen/debug/db.dart | 2 +- lib/widgets/fullscreen/debug/metadata.dart | 2 +- lib/widgets/fullscreen/fullscreen_body.dart | 1 + .../fullscreen/fullscreen_debug_page.dart | 2 +- .../fullscreen/info/basic_section.dart | 2 +- lib/widgets/fullscreen/info/common.dart | 90 ++++++++ lib/widgets/fullscreen/info/info_page.dart | 195 ++++++++---------- .../fullscreen/info/location_section.dart | 2 +- lib/widgets/fullscreen/info/maps/common.dart | 6 +- .../fullscreen/info/metadata_section.dart | 193 +++++++---------- .../fullscreen/info/notifications.dart | 10 + 15 files changed, 280 insertions(+), 233 deletions(-) create mode 100644 lib/widgets/fullscreen/info/common.dart create mode 100644 lib/widgets/fullscreen/info/notifications.dart diff --git a/lib/main.dart b/lib/main.dart index 452264f7c..e5ee4e35a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,6 +43,7 @@ class AvesApp extends StatefulWidget { class _AvesAppState extends State { Future _appSetup; + // observers are not registered when using the same list object with different items // the list itself needs to be reassigned List _navigatorObservers = []; diff --git a/lib/utils/durations.dart b/lib/utils/durations.dart index a5709e238..b8ba3b72d 100644 --- a/lib/utils/durations.dart +++ b/lib/utils/durations.dart @@ -7,6 +7,8 @@ class Durations { static const sweeperOpacityAnimation = Duration(milliseconds: 150); static const sweepingAnimation = Duration(milliseconds: 650); static const popupMenuAnimation = Duration(milliseconds: 300); // ref _PopupMenuRoute._kMenuDuration + static const dialogTransitionAnimation = Duration(milliseconds: 150); // ref `transitionDuration` in `showDialog()` + static const staggeredAnimation = Duration(milliseconds: 375); static const dialogFieldReachAnimation = Duration(milliseconds: 300); diff --git a/lib/widgets/app_debug_page.dart b/lib/widgets/app_debug_page.dart index 3c61a6fa9..362611baf 100644 --- a/lib/widgets/app_debug_page.dart +++ b/lib/widgets/app_debug_page.dart @@ -12,7 +12,7 @@ import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; diff --git a/lib/widgets/common/image_providers/thumbnail_provider.dart b/lib/widgets/common/image_providers/thumbnail_provider.dart index 90e4a8416..0a7f6784f 100644 --- a/lib/widgets/common/image_providers/thumbnail_provider.dart +++ b/lib/widgets/common/image_providers/thumbnail_provider.dart @@ -90,7 +90,8 @@ class ThumbnailProviderKey { return ThumbnailProviderKey( uri: entry.uri, mimeType: entry.mimeType, - dateModifiedSecs: entry.dateModifiedSecs ?? -1, // can happen in viewer mode + // `dateModifiedSecs` can be missing in viewer mode + dateModifiedSecs: entry.dateModifiedSecs ?? -1, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, extent: extent, diff --git a/lib/widgets/fullscreen/debug/db.dart b/lib/widgets/fullscreen/debug/db.dart index 3b085e300..ce2b89699 100644 --- a/lib/widgets/fullscreen/debug/db.dart +++ b/lib/widgets/fullscreen/debug/db.dart @@ -1,7 +1,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_metadata.dart'; import 'package:aves/model/metadata_db.dart'; -import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; class DbTab extends StatefulWidget { diff --git a/lib/widgets/fullscreen/debug/metadata.dart b/lib/widgets/fullscreen/debug/metadata.dart index fe097617c..e097d1c0d 100644 --- a/lib/widgets/fullscreen/debug/metadata.dart +++ b/lib/widgets/fullscreen/debug/metadata.dart @@ -5,7 +5,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; -import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; class MetadataTab extends StatefulWidget { diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index 840fb8e71..6e51042f0 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -11,6 +11,7 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart'; import 'package:aves/widgets/fullscreen/image_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/notifications.dart'; import 'package:aves/widgets/fullscreen/overlay/bottom.dart'; import 'package:aves/widgets/fullscreen/overlay/top.dart'; import 'package:aves/widgets/fullscreen/overlay/video.dart'; diff --git a/lib/widgets/fullscreen/fullscreen_debug_page.dart b/lib/widgets/fullscreen/fullscreen_debug_page.dart index dc4ce5786..74c1133aa 100644 --- a/lib/widgets/fullscreen/fullscreen_debug_page.dart +++ b/lib/widgets/fullscreen/fullscreen_debug_page.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart'; import 'package:aves/widgets/fullscreen/debug/db.dart'; import 'package:aves/widgets/fullscreen/debug/metadata.dart'; -import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index 498c41331..0c158273f 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -9,7 +9,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; -import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; diff --git a/lib/widgets/fullscreen/info/common.dart b/lib/widgets/fullscreen/info/common.dart new file mode 100644 index 000000000..a91078d9d --- /dev/null +++ b/lib/widgets/fullscreen/info/common.dart @@ -0,0 +1,90 @@ +import 'package:aves/widgets/common/aves_filter_chip.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class SectionRow extends StatelessWidget { + final IconData icon; + + const SectionRow(this.icon); + + @override + Widget build(BuildContext context) { + const dim = 32.0; + Widget buildDivider() => SizedBox( + width: dim, + child: Divider( + thickness: AvesFilterChip.outlineWidth, + color: Colors.white70, + ), + ); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildDivider(), + Padding( + padding: EdgeInsets.all(16), + child: Icon( + icon, + size: dim, + ), + ), + buildDivider(), + ], + ); + } +} + +class InfoRowGroup extends StatefulWidget { + final Map keyValues; + final int maxValueLength; + + const InfoRowGroup( + this.keyValues, { + this.maxValueLength = 0, + }); + + @override + _InfoRowGroupState createState() => _InfoRowGroupState(); +} + +class _InfoRowGroupState extends State { + final List _expandedKeys = []; + + Map get keyValues => widget.keyValues; + + int get maxValueLength => widget.maxValueLength; + + @override + Widget build(BuildContext context) { + if (keyValues.isEmpty) return SizedBox.shrink(); + final lastKey = keyValues.keys.last; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText.rich( + TextSpan( + children: keyValues.entries.expand( + (kv) { + final key = kv.key; + var value = kv.value; + final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key); + if (showPreviewOnly) { + value = '${value.substring(0, maxValueLength)}…'; + } + return [ + TextSpan(text: '$key ', style: TextStyle(color: Colors.white70, height: 1.7)), + TextSpan(text: '$value${key == lastKey ? '' : '\n'}', recognizer: showPreviewOnly ? _buildTapRecognizer(key) : null), + ]; + }, + ).toList(), + ), + style: TextStyle(fontFamily: 'Concourse'), + ), + ], + ); + } + + GestureRecognizer _buildTapRecognizer(String key) { + return TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key)); + } +} diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 33556a12a..df207102f 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -1,15 +1,19 @@ +import 'dart:collection'; + import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/services/metadata_service.dart'; import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/basic_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/info/metadata_section.dart'; -import 'package:flutter/gestures.dart'; +import 'package:aves/widgets/fullscreen/info/notifications.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -32,9 +36,44 @@ class InfoPage extends StatefulWidget { class InfoPageState extends State { final ScrollController _scrollController = ScrollController(); bool _scrollStartFromTop = false; + List _metadata = []; + String _loadedMetadataUri; CollectionLens get collection => widget.collection; + ImageEntry get entry => widget.entryNotifier.value; + + bool get isVisible => widget.visibleNotifier.value; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + _getMetadata(); + } + + @override + void didUpdateWidget(InfoPage oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + _getMetadata(); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(InfoPage widget) { + widget.visibleNotifier.addListener(_getMetadata); + } + + void _unregisterWidget(InfoPage widget) { + widget.visibleNotifier.removeListener(_getMetadata); + } + @override Widget build(BuildContext context) { const horizontalPadding = EdgeInsets.symmetric(horizontal: 8); @@ -96,22 +135,27 @@ class InfoPageState extends State { ); final metadataSliver = MetadataSectionSliver( entry: entry, - visibleNotifier: widget.visibleNotifier, + metadata: _metadata, ); - return CustomScrollView( - controller: _scrollController, - slivers: [ - appBar, - SliverPadding( - padding: horizontalPadding + EdgeInsets.only(top: 8), - sliver: basicAndLocationSliver, - ), - SliverPadding( - padding: horizontalPadding + EdgeInsets.only(bottom: 8 + mqViewInsetsBottom), - sliver: metadataSliver, - ), - ], + return AnimationLimiter( + // we update the limiter key after fetching the metadata of a new entry, + // in order to restart the staggered animation of the metadata section + key: Key(_loadedMetadataUri), + child: CustomScrollView( + controller: _scrollController, + slivers: [ + appBar, + SliverPadding( + padding: horizontalPadding + EdgeInsets.only(top: 8), + sliver: basicAndLocationSliver, + ), + SliverPadding( + padding: horizontalPadding + EdgeInsets.only(bottom: 8 + mqViewInsetsBottom), + sliver: metadataSliver, + ), + ], + ), ); }, ); @@ -159,99 +203,32 @@ class InfoPageState extends State { if (collection == null) return; FilterNotification(filter).dispatch(context); } -} -class SectionRow extends StatelessWidget { - final IconData icon; - - const SectionRow(this.icon); - - @override - Widget build(BuildContext context) { - const dim = 32.0; - Widget buildDivider() => SizedBox( - width: dim, - child: Divider( - thickness: AvesFilterChip.outlineWidth, - color: Colors.white70, - ), - ); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buildDivider(), - Padding( - padding: EdgeInsets.all(16), - child: Icon( - icon, - size: dim, - ), - ), - buildDivider(), - ], - ); + // fetch and hold metadata in the page widget and not in the section sliver, + // so that we can refresh and limit the staggered animation of the metadata section + Future _getMetadata() async { + if (entry == null) return; + if (_loadedMetadataUri == entry.uri) return; + if (isVisible) { + final rawMetadata = await MetadataService.getAllMetadata(entry) ?? {}; + _metadata = rawMetadata.entries.map((dirKV) { + final directoryName = dirKV.key as String ?? ''; + final rawTags = dirKV.value as Map ?? {}; + final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { + final value = tagKV.value as String ?? ''; + if (value.isEmpty) return null; + final tagName = tagKV.key as String ?? ''; + return MapEntry(tagName, value); + }).where((kv) => kv != null))); + return MetadataDirectory(directoryName, tags); + }).toList() + ..sort((a, b) => compareAsciiUpperCase(a.name, b.name)); + _loadedMetadataUri = entry.uri; + } else { + _metadata = []; + _loadedMetadataUri = null; + } + // _expandedDirectoryNotifier.value = null; + if (mounted) setState(() {}); } } - -class InfoRowGroup extends StatefulWidget { - final Map keyValues; - final int maxValueLength; - - const InfoRowGroup( - this.keyValues, { - this.maxValueLength = 0, - }); - - @override - _InfoRowGroupState createState() => _InfoRowGroupState(); -} - -class _InfoRowGroupState extends State { - final List _expandedKeys = []; - - Map get keyValues => widget.keyValues; - - int get maxValueLength => widget.maxValueLength; - - @override - Widget build(BuildContext context) { - if (keyValues.isEmpty) return SizedBox.shrink(); - final lastKey = keyValues.keys.last; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText.rich( - TextSpan( - children: keyValues.entries.expand( - (kv) { - final key = kv.key; - var value = kv.value; - final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key); - if (showPreviewOnly) { - value = '${value.substring(0, maxValueLength)}…'; - } - return [ - TextSpan(text: '$key ', style: TextStyle(color: Colors.white70, height: 1.7)), - TextSpan(text: '$value${key == lastKey ? '' : '\n'}', recognizer: showPreviewOnly ? _buildTapRecognizer(key) : null), - ]; - }, - ).toList(), - ), - style: TextStyle(fontFamily: 'Concourse'), - ), - ], - ); - } - - GestureRecognizer _buildTapRecognizer(String key) { - return TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key)); - } -} - -class BackUpNotification extends Notification {} - -class FilterNotification extends Notification { - final CollectionFilter filter; - - const FilterNotification(this.filter); -} diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 3bfb30cf0..6413da77d 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -5,7 +5,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:aves/widgets/fullscreen/info/maps/common.dart'; import 'package:aves/widgets/fullscreen/info/maps/google_map.dart'; import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart'; diff --git a/lib/widgets/fullscreen/info/maps/common.dart b/lib/widgets/fullscreen/info/maps/common.dart index d0d67dae5..de76c8c64 100644 --- a/lib/widgets/fullscreen/info/maps/common.dart +++ b/lib/widgets/fullscreen/info/maps/common.dart @@ -1,5 +1,6 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; +import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; @@ -7,6 +8,7 @@ import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; class MapDecorator extends StatelessWidget { final Widget child; @@ -76,7 +78,9 @@ class MapButtonPanel extends StatelessWidget { title: 'Map Style', ), ); - if (style != null) { + // wait for the dialog to hide because switching to Google Maps layer may block the UI + await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); + if (style != null && style != settings.infoMapStyle) { settings.infoMapStyle = style; MapStyleChangedNotification().dispatch(context); } diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata_section.dart index 6c89cad41..351d02d52 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata_section.dart @@ -1,174 +1,135 @@ -import 'dart:async'; import 'dart:collection'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/services/metadata_service.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:aves/widgets/fullscreen/info/metadata_thumbnail.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; class MetadataSectionSliver extends StatefulWidget { final ImageEntry entry; - final ValueNotifier visibleNotifier; + final List metadata; const MetadataSectionSliver({ @required this.entry, - @required this.visibleNotifier, + @required this.metadata, }); @override - State createState() => _MetadataSectionSliverState(); + State createState() => metadataSectionSliverState(); } -class _MetadataSectionSliverState extends State with AutomaticKeepAliveClientMixin { - List<_MetadataDirectory> _metadata = []; - String _loadedMetadataUri; +class metadataSectionSliverState extends State with AutomaticKeepAliveClientMixin { final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); ImageEntry get entry => widget.entry; - bool get isVisible => widget.visibleNotifier.value; + List get metadata => widget.metadata; // special directory names static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor static const xmpDirectory = 'XMP'; // from metadata-extractor static const mediaDirectory = 'Media'; // additional media (video/audio/images) directory - @override - void initState() { - super.initState(); - _registerWidget(widget); - _getMetadata(); - } - - @override - void didUpdateWidget(MetadataSectionSliver oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - _getMetadata(); - } - - @override - void dispose() { - _unregisterWidget(widget); - super.dispose(); - } - - void _registerWidget(MetadataSectionSliver widget) { - widget.visibleNotifier.addListener(_getMetadata); - } - - void _unregisterWidget(MetadataSectionSliver widget) { - widget.visibleNotifier.removeListener(_getMetadata); - } - @override Widget build(BuildContext context) { super.build(context); - if (_metadata.isEmpty) return SliverToBoxAdapter(child: SizedBox.shrink()); + if (metadata.isEmpty) return SliverToBoxAdapter(child: SizedBox.shrink()); - final directoriesWithoutTitle = _metadata.where((dir) => dir.name.isEmpty).toList(); - final directoriesWithTitle = _metadata.where((dir) => dir.name.isNotEmpty).toList(); + final directoriesWithoutTitle = metadata.where((dir) => dir.name.isEmpty).toList(); + final directoriesWithTitle = metadata.where((dir) => dir.name.isNotEmpty).toList(); final untitledDirectoryCount = directoriesWithoutTitle.length; return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { + Widget child; if (index == 0) { - return SectionRow(AIcons.info); + child = SectionRow(AIcons.info); + } else if (index < untitledDirectoryCount + 1) { + child = _buildDirTileWithoutTitle(directoriesWithoutTitle[index - 1]); + } else { + child = _buildDirTileWithTitle(directoriesWithTitle[index - 1 - untitledDirectoryCount]); } - if (index < untitledDirectoryCount + 1) { - final dir = directoriesWithoutTitle[index - 1]; - return InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength); - } - final dir = directoriesWithTitle[index - 1 - untitledDirectoryCount]; - Widget thumbnail; - final prefixChildren = []; - switch (dir.name) { - case exifThumbnailDirectory: - thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry); - break; - case xmpDirectory: - thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry); - break; - case mediaDirectory: - thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry); - Widget builder(IconData data) => Padding( - padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: Icon(data), - ); - if (dir.tags['Has Video'] == 'yes') prefixChildren.add(builder(AIcons.video)); - if (dir.tags['Has Audio'] == 'yes') prefixChildren.add(builder(AIcons.audio)); - if (dir.tags['Has Image'] == 'yes') { - int count; - if (dir.tags.containsKey('Image Count')) { - count = int.tryParse(dir.tags['Image Count']); - } - prefixChildren.addAll(List.generate(count ?? 1, (i) => builder(AIcons.image))); - } - break; - } - - return AvesExpansionTile( - title: dir.name, - expandedNotifier: _expandedDirectoryNotifier, - children: [ - if (prefixChildren.isNotEmpty) - Align( - alignment: AlignmentDirectional.topStart, - child: Wrap(children: prefixChildren), - ), - if (thumbnail != null) thumbnail, - Container( - alignment: Alignment.topLeft, - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength), + return AnimationConfiguration.staggeredList( + position: index, + duration: Durations.staggeredAnimation, + delay: Durations.staggeredAnimationDelay, + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, ), - ], + ), ); }, - childCount: 1 + _metadata.length, + childCount: 1 + metadata.length, ), ); } - Future _getMetadata() async { - if (_loadedMetadataUri == entry.uri) return; - if (isVisible) { - final rawMetadata = await MetadataService.getAllMetadata(entry) ?? {}; - _metadata = rawMetadata.entries.map((dirKV) { - final directoryName = dirKV.key as String ?? ''; - final rawTags = dirKV.value as Map ?? {}; - final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { - final value = tagKV.value as String ?? ''; - if (value.isEmpty) return null; - final tagName = tagKV.key as String ?? ''; - return MapEntry(tagName, value); - }).where((kv) => kv != null))); - return _MetadataDirectory(directoryName, tags); - }).toList() - ..sort((a, b) => compareAsciiUpperCase(a.name, b.name)); - _loadedMetadataUri = entry.uri; - } else { - _metadata = []; - _loadedMetadataUri = null; + Widget _buildDirTileWithoutTitle(MetadataDirectory dir) { + return InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength); + } + + Widget _buildDirTileWithTitle(MetadataDirectory dir) { + Widget thumbnail; + final prefixChildren = []; + switch (dir.name) { + case exifThumbnailDirectory: + thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry); + break; + case xmpDirectory: + thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry); + break; + case mediaDirectory: + thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry); + Widget builder(IconData data) => Padding( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Icon(data), + ); + if (dir.tags['Has Video'] == 'yes') prefixChildren.add(builder(AIcons.video)); + if (dir.tags['Has Audio'] == 'yes') prefixChildren.add(builder(AIcons.audio)); + if (dir.tags['Has Image'] == 'yes') { + int count; + if (dir.tags.containsKey('Image Count')) { + count = int.tryParse(dir.tags['Image Count']); + } + prefixChildren.addAll(List.generate(count ?? 1, (i) => builder(AIcons.image))); + } + break; } - _expandedDirectoryNotifier.value = null; - if (mounted) setState(() {}); + + return AvesExpansionTile( + title: dir.name, + expandedNotifier: _expandedDirectoryNotifier, + children: [ + if (prefixChildren.isNotEmpty) + Align( + alignment: AlignmentDirectional.topStart, + child: Wrap(children: prefixChildren), + ), + if (thumbnail != null) thumbnail, + Container( + alignment: Alignment.topLeft, + padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength), + ), + ], + ); } @override bool get wantKeepAlive => true; } -class _MetadataDirectory { +class MetadataDirectory { final String name; final SplayTreeMap tags; - const _MetadataDirectory(this.name, this.tags); + const MetadataDirectory(this.name, this.tags); } diff --git a/lib/widgets/fullscreen/info/notifications.dart b/lib/widgets/fullscreen/info/notifications.dart new file mode 100644 index 000000000..0f46e5aac --- /dev/null +++ b/lib/widgets/fullscreen/info/notifications.dart @@ -0,0 +1,10 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:flutter/material.dart'; + +class BackUpNotification extends Notification {} + +class FilterNotification extends Notification { + final CollectionFilter filter; + + const FilterNotification(this.filter); +}