aves/lib/widgets/viewer/overlay/bottom.dart
Thibault Deckers 0f1d8ec760 explorer fixes
2024-06-26 22:13:27 +02:00

315 lines
11 KiB
Dart

import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/viewer/controls/intents.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/multipage/controller.dart';
import 'package:aves/widgets/viewer/overlay/multipage.dart';
import 'package:aves/widgets/viewer/overlay/selection_button.dart';
import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart';
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class ViewerBottomOverlay extends StatefulWidget {
final List<AvesEntry> entries;
final int index;
final CollectionLens? collection;
final AnimationController animationController;
final Size availableSize;
final EdgeInsets? viewInsets, viewPadding;
final MultiPageController? multiPageController;
// always keep action buttons in the lower right corner, even with RTL locales
static const actionsDirection = TextDirection.ltr;
const ViewerBottomOverlay({
super.key,
required this.entries,
required this.index,
required this.collection,
required this.animationController,
required this.availableSize,
this.viewInsets,
this.viewPadding,
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
Widget build(BuildContext context) {
final mainEntry = entry;
if (mainEntry == null) return const SizedBox();
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
entries: entries,
index: widget.index,
mainEntry: mainEntry,
pageEntry: pageEntry ?? mainEntry,
collection: widget.collection,
availableSize: widget.availableSize,
viewInsets: widget.viewInsets,
viewPadding: widget.viewPadding,
multiPageController: multiPageController,
animationController: widget.animationController,
);
Widget child = multiPageController != null
? PageEntryBuilder(
multiPageController: multiPageController!,
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
)
: _buildContent();
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: child,
);
}
}
class _BottomOverlayContent extends StatefulWidget {
final List<AvesEntry> entries;
final int index;
final AvesEntry mainEntry, pageEntry;
final CollectionLens? collection;
final Size availableSize;
final EdgeInsets? viewInsets, viewPadding;
final MultiPageController? multiPageController;
final AnimationController animationController;
const _BottomOverlayContent({
required this.entries,
required this.index,
required this.mainEntry,
required this.pageEntry,
required this.collection,
required this.availableSize,
required this.viewInsets,
required this.viewPadding,
required this.multiPageController,
required this.animationController,
});
@override
State<_BottomOverlayContent> createState() => _BottomOverlayContentState();
}
class _BottomOverlayContentState extends State<_BottomOverlayContent> {
final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode();
late CurvedAnimation _buttonScale, _thumbnailOpacity;
@override
void initState() {
super.initState();
_registerWidget(widget);
WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus());
}
@override
void didUpdateWidget(covariant _BottomOverlayContent oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
_buttonRowFocusScopeNode.dispose();
super.dispose();
}
void _registerWidget(_BottomOverlayContent widget) {
final animationController = widget.animationController;
_buttonScale = CurvedAnimation(
parent: animationController,
// a little bounce at the top
curve: Curves.easeOutBack,
);
_thumbnailOpacity = CurvedAnimation(
parent: animationController,
curve: Curves.easeOutQuad,
);
}
void _unregisterWidget(_BottomOverlayContent widget) {
_buttonScale.dispose();
_thumbnailOpacity.dispose();
}
@override
Widget build(BuildContext context) {
final mainEntry = widget.mainEntry;
final pageEntry = widget.pageEntry;
final multiPageController = widget.multiPageController;
final isWallpaperMode = context.read<ValueNotifier<AppMode>>().value == AppMode.setWallpaper;
return AnimatedBuilder(
animation: Listenable.merge([
mainEntry.metadataChangeNotifier,
pageEntry.metadataChangeNotifier,
]),
builder: (context, child) {
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
final selection = context.read<Selection<AvesEntry>?>();
final viewerButtonRow = (selection?.isSelecting ?? false)
? SelectionButton(
mainEntry: mainEntry,
scale: _buttonScale,
)
: FocusableActionDetector(
focusNode: _buttonRowFocusScopeNode,
shortcuts: settings.useTvLayout
? const {
SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent(),
}
: null,
actions: {
TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)),
},
child: SafeArea(
top: false,
bottom: false,
minimum: EdgeInsets.only(
left: viewInsetsPadding.left,
right: viewInsetsPadding.right,
),
child: isWallpaperMode
? WallpaperButtons(
entry: pageEntry,
scale: _buttonScale,
)
: ViewerButtons(
mainEntry: mainEntry,
pageEntry: pageEntry,
collection: widget.collection,
scale: _buttonScale,
),
),
);
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
final collapsedPageScroller = mainEntry.isMotionPhoto;
final availableWidth = widget.availableSize.width;
return SizedBox(
width: availableWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showMultiPageOverlay && !collapsedPageScroller)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FadeTransition(
opacity: _thumbnailOpacity,
child: MultiPageOverlay(
controller: multiPageController,
availableWidth: availableWidth,
scrollable: true,
),
),
),
(showMultiPageOverlay && collapsedPageScroller)
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
textDirection: ViewerBottomOverlay.actionsDirection,
children: [
SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MultiPageOverlay(
controller: multiPageController,
availableWidth: availableWidth,
scrollable: false,
),
),
),
Expanded(child: viewerButtonRow),
],
)
: viewerButtonRow,
if (settings.showOverlayThumbnailPreview && !isWallpaperMode)
FadeTransition(
opacity: _thumbnailOpacity,
child: ViewerThumbnailPreview(
availableWidth: availableWidth,
displayedIndex: widget.index,
entries: widget.entries,
),
),
],
),
);
},
);
}
void _requestFocus() => _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus();
}
class ExtraBottomOverlay extends StatelessWidget {
final EdgeInsets? viewInsets, viewPadding;
final Widget child;
const ExtraBottomOverlay({
super.key,
this.viewInsets,
this.viewPadding,
required this.child,
});
@override
Widget build(BuildContext context) {
final viewInsets = this.viewInsets ?? MediaQuery.viewInsetsOf(context);
final viewPadding = this.viewPadding ?? MediaQuery.viewPaddingOf(context);
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + const EdgeInsets.symmetric(horizontal: 8);
return Padding(
padding: safePadding,
child: SizedBox(
width: MediaQuery.sizeOf(context).width - safePadding.horizontal,
child: child,
),
);
}
}