#1177 slideshow: fixed bottom overlay layout on inset transition
This commit is contained in:
parent
013b2631aa
commit
535d4c0d00
3 changed files with 144 additions and 72 deletions
|
@ -412,17 +412,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSlideshowBottomOverlay(Size availableSize) {
|
Widget _buildSlideshowBottomOverlay(Size availableSize) {
|
||||||
return SizedBox.fromSize(
|
return TooltipTheme(
|
||||||
size: availableSize,
|
data: TooltipTheme.of(context).copyWith(
|
||||||
|
preferBelow: false,
|
||||||
|
),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
alignment: AlignmentDirectional.bottomEnd,
|
||||||
child: TooltipTheme(
|
child: SlideshowBottomOverlay(
|
||||||
data: TooltipTheme.of(context).copyWith(
|
animationController: _overlayAnimationController,
|
||||||
preferBelow: false,
|
availableSize: availableSize,
|
||||||
),
|
viewInsets: _frozenViewInsets,
|
||||||
child: SlideshowButtons(
|
viewPadding: _frozenViewPadding,
|
||||||
animationController: _overlayAnimationController,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ViewerBottomOverlay extends StatefulWidget {
|
class ViewerBottomOverlay extends StatelessWidget {
|
||||||
final List<AvesEntry> entries;
|
final List<AvesEntry> entries;
|
||||||
final int index;
|
final int index;
|
||||||
final CollectionLens? collection;
|
final CollectionLens? collection;
|
||||||
|
@ -33,6 +33,10 @@ class ViewerBottomOverlay extends StatefulWidget {
|
||||||
// always keep action buttons in the lower right corner, even with RTL locales
|
// always keep action buttons in the lower right corner, even with RTL locales
|
||||||
static const actionsDirection = TextDirection.ltr;
|
static const actionsDirection = TextDirection.ltr;
|
||||||
|
|
||||||
|
AvesEntry? get entry {
|
||||||
|
return index < entries.length ? entries[index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
const ViewerBottomOverlay({
|
const ViewerBottomOverlay({
|
||||||
super.key,
|
super.key,
|
||||||
required this.entries,
|
required this.entries,
|
||||||
|
@ -45,27 +49,6 @@ class ViewerBottomOverlay extends StatefulWidget {
|
||||||
required this.multiPageController,
|
required this.multiPageController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _ViewerBottomOverlayState();
|
|
||||||
|
|
||||||
static double actionSafeHeight(BuildContext context) {
|
|
||||||
final mqPaddingBottom = context.select<MediaQueryData, double>((mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom));
|
|
||||||
final buttonHeight = ViewerButtons.preferredHeight(context);
|
|
||||||
final thumbnailHeight = (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0);
|
|
||||||
return mqPaddingBottom + buttonHeight + thumbnailHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
|
||||||
List<AvesEntry> get entries => widget.entries;
|
|
||||||
|
|
||||||
AvesEntry? get entry {
|
|
||||||
final index = widget.index;
|
|
||||||
return index < entries.length ? entries[index] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiPageController? get multiPageController => widget.multiPageController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mainEntry = entry;
|
final mainEntry = entry;
|
||||||
|
@ -73,15 +56,15 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
||||||
|
|
||||||
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
|
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
|
||||||
entries: entries,
|
entries: entries,
|
||||||
index: widget.index,
|
index: index,
|
||||||
mainEntry: mainEntry,
|
mainEntry: mainEntry,
|
||||||
pageEntry: pageEntry ?? mainEntry,
|
pageEntry: pageEntry ?? mainEntry,
|
||||||
collection: widget.collection,
|
collection: collection,
|
||||||
availableSize: widget.availableSize,
|
availableSize: availableSize,
|
||||||
viewInsets: widget.viewInsets,
|
viewInsets: viewInsets,
|
||||||
viewPadding: widget.viewPadding,
|
viewPadding: viewPadding,
|
||||||
multiPageController: multiPageController,
|
multiPageController: multiPageController,
|
||||||
animationController: widget.animationController,
|
animationController: animationController,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget child = multiPageController != null
|
Widget child = multiPageController != null
|
||||||
|
@ -102,6 +85,13 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double actionSafeHeight(BuildContext context) {
|
||||||
|
final mqPaddingBottom = context.select<MediaQueryData, double>((mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom));
|
||||||
|
final buttonHeight = ViewerButtons.preferredHeight(context);
|
||||||
|
final thumbnailHeight = (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0);
|
||||||
|
return mqPaddingBottom + buttonHeight + thumbnailHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BottomOverlayContent extends StatefulWidget {
|
class _BottomOverlayContent extends StatefulWidget {
|
||||||
|
|
|
@ -1,21 +1,64 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/view/view.dart';
|
import 'package:aves/view/view.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
|
||||||
import 'package:aves/widgets/viewer/controls/intents.dart';
|
import 'package:aves/widgets/viewer/controls/intents.dart';
|
||||||
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||||||
|
import 'package:aves/widgets/viewer/overlay/bottom.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
|
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
|
||||||
import 'package:aves/widgets/viewer/slideshow_page.dart';
|
import 'package:aves/widgets/viewer/slideshow_page.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class SlideshowBottomOverlay extends StatelessWidget {
|
||||||
|
final AnimationController animationController;
|
||||||
|
final Size availableSize;
|
||||||
|
final EdgeInsets? viewInsets, viewPadding;
|
||||||
|
|
||||||
|
const SlideshowBottomOverlay({
|
||||||
|
super.key,
|
||||||
|
required this.animationController,
|
||||||
|
required this.availableSize,
|
||||||
|
this.viewInsets,
|
||||||
|
this.viewPadding,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<MediaQueryData, double>(
|
||||||
|
selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom),
|
||||||
|
builder: (context, mqPaddingBottom, child) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: mqPaddingBottom),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: SlideshowButtons(
|
||||||
|
availableSize: availableSize,
|
||||||
|
viewInsets: viewInsets,
|
||||||
|
viewPadding: viewPadding,
|
||||||
|
animationController: animationController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SlideshowButtons extends StatefulWidget {
|
class SlideshowButtons extends StatefulWidget {
|
||||||
|
final Size availableSize;
|
||||||
|
final EdgeInsets? viewInsets, viewPadding;
|
||||||
final AnimationController animationController;
|
final AnimationController animationController;
|
||||||
|
|
||||||
const SlideshowButtons({
|
const SlideshowButtons({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.availableSize,
|
||||||
|
required this.viewInsets,
|
||||||
|
required this.viewPadding,
|
||||||
required this.animationController,
|
required this.animationController,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -70,7 +113,8 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FocusableActionDetector(
|
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
||||||
|
final viewerButtonRow = FocusableActionDetector(
|
||||||
focusNode: _buttonRowFocusScopeNode,
|
focusNode: _buttonRowFocusScopeNode,
|
||||||
shortcuts: settings.useTvLayout
|
shortcuts: settings.useTvLayout
|
||||||
? const {
|
? const {
|
||||||
|
@ -80,40 +124,78 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
|
||||||
actions: {
|
actions: {
|
||||||
TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)),
|
TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)),
|
||||||
},
|
},
|
||||||
child: settings.useTvLayout
|
child: SafeArea(
|
||||||
? Row(
|
top: false,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
bottom: false,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
minimum: EdgeInsets.only(
|
||||||
children: _actions.map((action) {
|
left: viewInsetsPadding.left,
|
||||||
return CaptionedButton(
|
right: viewInsetsPadding.right,
|
||||||
scale: _buttonScale,
|
),
|
||||||
icon: action.getIcon(),
|
child: _buildButtons(context),
|
||||||
caption: action.getText(context),
|
),
|
||||||
onPressed: () => _onAction(context, action),
|
);
|
||||||
);
|
|
||||||
}).toList(),
|
final availableWidth = widget.availableSize.width;
|
||||||
)
|
return SizedBox(
|
||||||
: SafeArea(
|
width: availableWidth,
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.only(left: _padding / 2, right: _padding / 2, bottom: _padding),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: _actions
|
viewerButtonRow,
|
||||||
.map((action) => Padding(
|
],
|
||||||
padding: const EdgeInsets.symmetric(horizontal: _padding / 2),
|
),
|
||||||
child: OverlayButton(
|
);
|
||||||
scale: _buttonScale,
|
}
|
||||||
child: IconButton(
|
|
||||||
icon: action.getIcon(),
|
Widget _buildButtons(BuildContext context) {
|
||||||
onPressed: () => _onAction(context, action),
|
if (settings.useTvLayout) {
|
||||||
tooltip: action.getText(context),
|
return _buildTvButtonRowContent(context);
|
||||||
),
|
}
|
||||||
),
|
|
||||||
))
|
return SafeArea(
|
||||||
.toList(),
|
top: false,
|
||||||
),
|
bottom: false,
|
||||||
),
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.only(left: _padding / 2, right: _padding / 2, bottom: _padding),
|
||||||
|
child: Row(
|
||||||
|
textDirection: ViewerBottomOverlay.actionsDirection,
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
..._actions.map((action) => _buildOverlayButton(context, action)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOverlayButton(BuildContext context, SlideshowAction action) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: _padding / 2),
|
||||||
|
child: OverlayButton(
|
||||||
|
scale: _buttonScale,
|
||||||
|
child: IconButton(
|
||||||
|
icon: action.getIcon(),
|
||||||
|
onPressed: () => _onAction(context, action),
|
||||||
|
tooltip: action.getText(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTvButtonRowContent(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
textDirection: ViewerBottomOverlay.actionsDirection,
|
||||||
|
children: _actions.map((action) {
|
||||||
|
return CaptionedButton(
|
||||||
|
scale: _buttonScale,
|
||||||
|
icon: action.getIcon(),
|
||||||
|
caption: action.getText(context),
|
||||||
|
onPressed: () => _onAction(context, action),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue