#503 viewer: overlay details expand/collapse on tap
This commit is contained in:
parent
b379cfaa21
commit
e2229e8967
7 changed files with 128 additions and 52 deletions
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Viewer: overlay details expand/collapse on tap
|
||||
|
||||
### Fixed
|
||||
|
||||
- SD card access grant on Android Lollipop
|
||||
|
|
|
@ -64,6 +64,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
final AChangeNotifier _verticalScrollNotifier = AChangeNotifier();
|
||||
bool _overlayInitialized = false;
|
||||
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
|
||||
final ValueNotifier<bool> _overlayExpandedNotifier = ValueNotifier(false);
|
||||
late AnimationController _overlayAnimationController;
|
||||
late Animation<double> _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity;
|
||||
late Animation<Offset> _overlayTopOffset;
|
||||
|
@ -161,8 +162,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> 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<EntryViewerStack> with EntryViewContr
|
|||
child: ViewerTopOverlay(
|
||||
entries: entries,
|
||||
index: _currentEntryIndex,
|
||||
hasCollection: hasCollection,
|
||||
mainEntry: mainEntry,
|
||||
scale: _overlayButtonScale,
|
||||
hasCollection: hasCollection,
|
||||
expandedNotifier: _overlayExpandedNotifier,
|
||||
availableSize: availableSize,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<bool> 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<ViewerDetailOverlay> {
|
|||
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<bool> 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 = <Widget>[];
|
||||
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(
|
||||
|
|
33
lib/widgets/viewer/overlay/details/expander.dart
Normal file
33
lib/widgets/viewer/overlay/details/expander.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class OverlayRowExpander extends StatelessWidget {
|
||||
final ValueNotifier<bool> expandedNotifier;
|
||||
final Widget child;
|
||||
|
||||
const OverlayRowExpander({
|
||||
super.key,
|
||||
required this.expandedNotifier,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<DurationsData, Duration>((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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
final AvesEntry mainEntry;
|
||||
final Animation<double> scale;
|
||||
final bool hasCollection;
|
||||
final ValueNotifier<bool> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue