From e2229e89671a0ae18289e20a4a0efbb2bee8dbfb Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 26 Jan 2023 23:04:56 +0100 Subject: [PATCH] #503 viewer: overlay details expand/collapse on tap --- CHANGELOG.md | 4 ++ lib/widgets/viewer/entry_viewer_stack.dart | 10 +++-- .../viewer/overlay/details/description.dart | 24 ++++++++--- .../viewer/overlay/details/details.dart | 27 ++++++++++-- .../viewer/overlay/details/expander.dart | 33 +++++++++++++++ .../viewer/overlay/details/rating_tags.dart | 42 +++++++++---------- lib/widgets/viewer/overlay/top.dart | 40 ++++++++++-------- 7 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 lib/widgets/viewer/overlay/details/expander.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b3fce214a..ab919d4eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Viewer: overlay details expand/collapse on tap + ### Fixed - SD card access grant on Android Lollipop diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 2012a1778..3dfb0f3dc 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -64,6 +64,7 @@ class _EntryViewerStackState extends State with EntryViewContr final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); bool _overlayInitialized = false; final ValueNotifier _overlayVisible = ValueNotifier(true); + final ValueNotifier _overlayExpandedNotifier = ValueNotifier(false); late AnimationController _overlayAnimationController; late Animation _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity; late Animation _overlayTopOffset; @@ -161,8 +162,10 @@ class _EntryViewerStackState extends State with EntryViewContr cleanEntryControllers(entryNotifier.value); _videoActionDelegate.dispose(); _overlayAnimationController.dispose(); - _overlayVisible.removeListener(_onOverlayVisibleChanged); - _verticalPager.removeListener(_onVerticalPageControllerChanged); + _overlayVisible.dispose(); + _overlayExpandedNotifier.dispose(); + _verticalPager.dispose(); + _heroInfoNotifier.dispose(); WidgetsBinding.instance.removeObserver(this); _unregisterWidget(widget); super.dispose(); @@ -295,9 +298,10 @@ class _EntryViewerStackState extends State with EntryViewContr child: ViewerTopOverlay( entries: entries, index: _currentEntryIndex, - hasCollection: hasCollection, mainEntry: mainEntry, scale: _overlayButtonScale, + hasCollection: hasCollection, + expandedNotifier: _overlayExpandedNotifier, availableSize: availableSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, diff --git a/lib/widgets/viewer/overlay/details/description.dart b/lib/widgets/viewer/overlay/details/description.dart index 02a476fdf..fec7ab219 100644 --- a/lib/widgets/viewer/overlay/details/description.dart +++ b/lib/widgets/viewer/overlay/details/description.dart @@ -14,12 +14,24 @@ class OverlayDescriptionRow extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - children: [ - DecoratedIcon(AIcons.description, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), - const SizedBox(width: ViewerDetailOverlayContent.iconPadding), - Expanded(child: Text(description, strutStyle: Constants.overflowStrutStyle)), - ], + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: ViewerDetailOverlayContent.iconPadding), + child: DecoratedIcon( + AIcons.description, + size: ViewerDetailOverlayContent.iconSize, + shadows: ViewerDetailOverlayContent.shadows(context), + ), + ), + ), + TextSpan(text: description), + ], + ), + strutStyle: Constants.overflowStrutStyle, ); } } diff --git a/lib/widgets/viewer/overlay/details/details.dart b/lib/widgets/viewer/overlay/details/details.dart index 0e7be08e7..d24265ee7 100644 --- a/lib/widgets/viewer/overlay/details/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -9,6 +9,7 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/details/date.dart'; import 'package:aves/widgets/viewer/overlay/details/description.dart'; +import 'package:aves/widgets/viewer/overlay/details/expander.dart'; import 'package:aves/widgets/viewer/overlay/details/location.dart'; import 'package:aves/widgets/viewer/overlay/details/position_title.dart'; import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart'; @@ -23,6 +24,7 @@ class ViewerDetailOverlay extends StatefulWidget { final int index; final bool hasCollection; final MultiPageController? multiPageController; + final ValueNotifier expandedNotifier; final Size availableSize; const ViewerDetailOverlay({ @@ -31,6 +33,7 @@ class ViewerDetailOverlay extends StatefulWidget { required this.index, required this.hasCollection, required this.multiPageController, + required this.expandedNotifier, required this.availableSize, }); @@ -102,6 +105,7 @@ class _ViewerDetailOverlayState extends State { position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null, availableWidth: widget.availableSize.width, multiPageController: multiPageController, + expandedNotifier: widget.expandedNotifier, ); return multiPageController != null @@ -123,6 +127,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { final String? position; final double availableWidth; final MultiPageController? multiPageController; + final ValueNotifier expandedNotifier; static const double _interRowPadding = 2.0; static const double _subRowMinWidth = 300.0; @@ -140,6 +145,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { required this.position, required this.availableWidth, required this.multiPageController, + required this.expandedNotifier, }); @override @@ -152,7 +158,11 @@ class ViewerDetailOverlayContent extends StatelessWidget { return AnimatedBuilder( animation: pageEntry.metadataChangeNotifier, builder: (context, child) { - final positionTitle = OverlayPositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); + final positionTitle = OverlayPositionTitleRow( + entry: pageEntry, + collectionPosition: position, + multiPageController: multiPageController, + ); return DefaultTextStyle( style: Theme.of(context).textTheme.bodyMedium!.copyWith( shadows: shadows(context), @@ -172,7 +182,10 @@ class ViewerDetailOverlayContent extends StatelessWidget { final rows = []; if (positionTitle.isNotEmpty) { - rows.add(positionTitle); + rows.add(OverlayRowExpander( + expandedNotifier: expandedNotifier, + child: positionTitle, + )); rows.add(const SizedBox(height: _interRowPadding)); } if (twoColumns) { @@ -225,13 +238,19 @@ class ViewerDetailOverlayContent extends StatelessWidget { Widget _buildRatingTagsFullRow(BuildContext context) => _buildFullRowSwitcher( context: context, visible: pageEntry.rating != 0 || pageEntry.tags.isNotEmpty, - builder: (context) => OverlayRatingTagsRow(entry: pageEntry), + builder: (context) => OverlayRowExpander( + expandedNotifier: expandedNotifier, + child: OverlayRatingTagsRow(entry: pageEntry), + ), ); Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher( context: context, visible: description != null, - builder: (context) => OverlayDescriptionRow(description: description!), + builder: (context) => OverlayRowExpander( + expandedNotifier: expandedNotifier, + child: OverlayDescriptionRow(description: description!), + ), ); Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher( diff --git a/lib/widgets/viewer/overlay/details/expander.dart b/lib/widgets/viewer/overlay/details/expander.dart new file mode 100644 index 000000000..592546145 --- /dev/null +++ b/lib/widgets/viewer/overlay/details/expander.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class OverlayRowExpander extends StatelessWidget { + final ValueNotifier expandedNotifier; + final Widget child; + + const OverlayRowExpander({ + super.key, + required this.expandedNotifier, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: expandedNotifier, + builder: (context, expanded, child) { + final parent = DefaultTextStyle.of(context); + return DefaultTextStyle( + key: key, + style: parent.style, + textAlign: parent.textAlign, + softWrap: expanded, + overflow: parent.overflow, + maxLines: expanded ? null : 42, + textWidthBasis: parent.textWidthBasis, + child: child!, + ); + }, + child: child, + ); + } +} diff --git a/lib/widgets/viewer/overlay/details/rating_tags.dart b/lib/widgets/viewer/overlay/details/rating_tags.dart index f665664fd..f99c222a1 100644 --- a/lib/widgets/viewer/overlay/details/rating_tags.dart +++ b/lib/widgets/viewer/overlay/details/rating_tags.dart @@ -1,14 +1,11 @@ import 'package:aves/model/entry.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/basic/text/animated_diff.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; import 'package:collection/collection.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class OverlayRatingTagsRow extends AnimatedWidget { final AvesEntry entry; @@ -37,27 +34,28 @@ class OverlayRatingTagsRow extends AnimatedWidget { final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural); final hasTags = tags.isNotEmpty; - final animationDuration = context.select((v) => v.textDiffAnimation); - return Row( - children: [ - AnimatedDiffText( - ratingString, - strutStyle: Constants.overflowStrutStyle, - duration: animationDuration, - ), - if (hasTags) ...[ - if (ratingString.isNotEmpty) const Text(Constants.separator), - DecoratedIcon(AIcons.tag, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), - const SizedBox(width: ViewerDetailOverlayContent.iconPadding), - Expanded( - child: AnimatedDiffText( - tags.join(Constants.separator), - strutStyle: Constants.overflowStrutStyle, - duration: animationDuration, + return Text.rich( + TextSpan( + children: [ + TextSpan(text: ratingString), + if (hasTags) ...[ + if (ratingString.isNotEmpty) const TextSpan(text: Constants.separator), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: ViewerDetailOverlayContent.iconPadding), + child: DecoratedIcon( + AIcons.tag, + size: ViewerDetailOverlayContent.iconSize, + shadows: ViewerDetailOverlayContent.shadows(context), + ), + ), ), - ), + TextSpan(text: tags.join(Constants.separator)), + ] ], - ], + ), + strutStyle: Constants.overflowStrutStyle, ); } } diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 3d3e8ddaa..7021e0149 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -16,6 +16,7 @@ class ViewerTopOverlay extends StatelessWidget { final AvesEntry mainEntry; final Animation scale; final bool hasCollection; + final ValueNotifier expandedNotifier; final Size availableSize; final EdgeInsets? viewInsets, viewPadding; @@ -26,6 +27,7 @@ class ViewerTopOverlay extends StatelessWidget { required this.mainEntry, required this.scale, required this.hasCollection, + required this.expandedNotifier, required this.availableSize, required this.viewInsets, required this.viewPadding, @@ -51,23 +53,27 @@ class ViewerTopOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showInfo) - BlurredRect( - enabled: blurred, - child: Container( - color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), - child: SafeArea( - bottom: false, - minimum: EdgeInsets.only( - left: viewInsetsPadding.left, - top: viewInsetsPadding.top, - right: viewInsetsPadding.right, - ), - child: ViewerDetailOverlay( - index: index, - entries: entries, - hasCollection: hasCollection, - multiPageController: multiPageController, - availableSize: availableSize, + GestureDetector( + onTap: () => expandedNotifier.value = !expandedNotifier.value, + child: BlurredRect( + enabled: blurred, + child: Container( + color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), + child: SafeArea( + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + top: viewInsetsPadding.top, + right: viewInsetsPadding.right, + ), + child: ViewerDetailOverlay( + index: index, + entries: entries, + hasCollection: hasCollection, + multiPageController: multiPageController, + expandedNotifier: expandedNotifier, + availableSize: availableSize, + ), ), ), ),