diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 75ad1e238..9e9e4a6a1 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -1,9 +1,6 @@ -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/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -11,9 +8,7 @@ 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: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'; @@ -146,48 +141,12 @@ class _InfoPageContent extends StatefulWidget { } class _InfoPageContentState extends State<_InfoPageContent> { - List _metadata = []; - String _loadedMetadataUri; - static const horizontalPadding = EdgeInsets.symmetric(horizontal: 8); CollectionLens get collection => widget.collection; ImageEntry get entry => widget.entry; - bool get isVisible => widget.visibleNotifier.value; - - @override - void initState() { - super.initState(); - _registerWidget(widget); - _getMetadata(); - } - - @override - void didUpdateWidget(_InfoPageContent oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - _getMetadata(); - } - - @override - void dispose() { - _unregisterWidget(widget); - super.dispose(); - } - - void _registerWidget(_InfoPageContent widget) { - widget.visibleNotifier.addListener(_getMetadata); - widget.entry.metadataChangeNotifier.addListener(_onMetadataChanged); - } - - void _unregisterWidget(_InfoPageContent widget) { - widget.visibleNotifier.removeListener(_getMetadata); - widget.entry.metadataChangeNotifier.removeListener(_onMetadataChanged); - } - @override Widget build(BuildContext context) { final locationAtTop = widget.split && entry.hasGps; @@ -219,27 +178,22 @@ class _InfoPageContentState extends State<_InfoPageContent> { ); final metadataSliver = MetadataSectionSliver( entry: entry, - metadata: _metadata, + visibleNotifier: widget.visibleNotifier, ); - 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: widget.scrollController, - slivers: [ - widget.appBar, - SliverPadding( - padding: horizontalPadding + EdgeInsets.only(top: 8), - sliver: basicAndLocationSliver, - ), - SliverPadding( - padding: horizontalPadding + EdgeInsets.only(bottom: 8 + widget.mqViewInsetsBottom), - sliver: metadataSliver, - ), - ], - ), + return CustomScrollView( + controller: widget.scrollController, + slivers: [ + widget.appBar, + SliverPadding( + padding: horizontalPadding + EdgeInsets.only(top: 8), + sliver: basicAndLocationSliver, + ), + SliverPadding( + padding: horizontalPadding + EdgeInsets.only(bottom: 8 + widget.mqViewInsetsBottom), + sliver: metadataSliver, + ), + ], ); } @@ -247,38 +201,4 @@ class _InfoPageContentState extends State<_InfoPageContent> { if (collection == null) return; FilterNotification(filter).dispatch(context); } - - void _onMetadataChanged() { - _metadata = []; - _loadedMetadataUri = null; - _getMetadata(); - } - - // 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(() {}); - } } diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata_section.dart index 351d02d52..eb5be71e3 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata_section.dart @@ -1,82 +1,127 @@ 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/common.dart'; import 'package:aves/widgets/fullscreen/info/metadata_thumbnail.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; class MetadataSectionSliver extends StatefulWidget { final ImageEntry entry; - final List metadata; + final ValueNotifier visibleNotifier; const MetadataSectionSliver({ @required this.entry, - @required this.metadata, + @required this.visibleNotifier, }); @override - State createState() => metadataSectionSliverState(); + State createState() => _MetadataSectionSliverState(); } -class metadataSectionSliverState extends State with AutomaticKeepAliveClientMixin { +class _MetadataSectionSliverState extends State with AutomaticKeepAliveClientMixin { + List<_MetadataDirectory> _metadata = []; + final ValueNotifier _loadedMetadataUri = ValueNotifier(null); final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); ImageEntry get entry => widget.entry; - List get metadata => widget.metadata; + bool get isVisible => widget.visibleNotifier.value; // 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); + widget.entry.metadataChangeNotifier.addListener(_onMetadataChanged); + } + + void _unregisterWidget(MetadataSectionSliver widget) { + widget.visibleNotifier.removeListener(_getMetadata); + widget.entry.metadataChangeNotifier.removeListener(_onMetadataChanged); + } + @override Widget build(BuildContext context) { super.build(context); - - 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 untitledDirectoryCount = directoriesWithoutTitle.length; - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - Widget child; - if (index == 0) { - child = SectionRow(AIcons.info); - } else if (index < untitledDirectoryCount + 1) { - child = _buildDirTileWithoutTitle(directoriesWithoutTitle[index - 1]); + // use a `Column` inside a `SliverToBoxAdapter`, instead of a `SliverList`, + // so that we can have the metadata-dependent `AnimationLimiter` inside the metadata section + // warning: placing the `AnimationLimiter` as a parent to the `ScrollView` + // triggers dispose & reinitialization of other sections, including heavy widgets like maps + return SliverToBoxAdapter( + child: AnimatedBuilder( + animation: _loadedMetadataUri, + builder: (context, child) { + Widget content; + if (_metadata.isEmpty) { + content = SizedBox.shrink(); } else { - child = _buildDirTileWithTitle(directoriesWithTitle[index - 1 - untitledDirectoryCount]); - } - return AnimationConfiguration.staggeredList( - position: index, - duration: Durations.staggeredAnimation, - delay: Durations.staggeredAnimationDelay, - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, + final directoriesWithoutTitle = _metadata.where((dir) => dir.name.isEmpty).toList(); + final directoriesWithTitle = _metadata.where((dir) => dir.name.isNotEmpty).toList(); + content = Column( + children: AnimationConfiguration.toStaggeredList( + duration: Durations.staggeredAnimation, + delay: Durations.staggeredAnimationDelay, + childAnimationBuilder: (child) => SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, + ), + ), + children: [ + SectionRow(AIcons.info), + ...directoriesWithoutTitle.map(_buildDirTileWithoutTitle), + ...directoriesWithTitle.map(_buildDirTileWithTitle), + ], ), - ), + ); + } + 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.value), + child: content, ); }, - childCount: 1 + metadata.length, ), ); } - Widget _buildDirTileWithoutTitle(MetadataDirectory dir) { + Widget _buildDirTileWithoutTitle(_MetadataDirectory dir) { return InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength); } - Widget _buildDirTileWithTitle(MetadataDirectory dir) { + Widget _buildDirTileWithTitle(_MetadataDirectory dir) { Widget thumbnail; final prefixChildren = []; switch (dir.name) { @@ -123,13 +168,43 @@ class metadataSectionSliverState extends State with Autom ); } + void _onMetadataChanged() { + _loadedMetadataUri.value = null; + _metadata = []; + _getMetadata(); + } + + Future _getMetadata() async { + if (entry == null) return; + if (_loadedMetadataUri.value == 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.value = entry.uri; + } else { + _metadata = []; + _loadedMetadataUri.value = null; + } + } + @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/stats/stats.dart b/lib/widgets/stats/stats.dart index f2a7dbaa3..4239b5239 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -16,6 +16,7 @@ import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; @@ -262,6 +263,6 @@ class EntryByMimeDatum { @override String toString() { - return '[$runtimeType#$hashCode: mimeType=$mimeType, displayText=$displayText, entryCount=$entryCount]'; + return '[$runtimeType#${shortHash(this)}: mimeType=$mimeType, displayText=$displayText, entryCount=$entryCount]'; } }