import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/theme.dart'; import 'package:aves/widgets/viewer/action/video_action_delegate.dart'; import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/entry_horizontal_pager.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/overlay/bottom.dart'; import 'package:aves/widgets/viewer/overlay/video/video.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/providers.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/visual/controller_mixin.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class WallpaperPage extends StatelessWidget { static const routeName = '/set_wallpaper'; final AvesEntry? entry; const WallpaperPage({ super.key, required this.entry, }); @override Widget build(BuildContext context) { return AvesScaffold( body: entry != null ? MultiProvider( providers: [ ViewStateConductorProvider(), VideoConductorProvider(), MultiPageConductorProvider(), ], child: EntryEditor( entry: entry!, ), ) : const SizedBox(), backgroundColor: Theme.of(context).isDark ? Colors.black : Colors.white, resizeToAvoidBottomInset: false, ); } } class EntryEditor extends StatefulWidget { final AvesEntry entry; const EntryEditor({ super.key, required this.entry, }); @override State createState() => _EntryEditorState(); } class _EntryEditorState extends State with EntryViewControllerMixin, SingleTickerProviderStateMixin { final ValueNotifier _overlayVisible = ValueNotifier(true); late AnimationController _overlayAnimationController; late CurvedAnimation _overlayVideoControlScale; EdgeInsets? _frozenViewInsets, _frozenViewPadding; late VideoActionDelegate _videoActionDelegate; late final ViewerController _viewerController; @override bool get isViewingImage => true; @override final ValueNotifier entryNotifier = ValueNotifier(null); AvesEntry get entry => widget.entry; @override void initState() { super.initState(); if (settings.maxBrightness == MaxBrightness.viewerOnly) { AvesApp.screenBrightness?.setApplicationScreenBrightness(1); } if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { windowService.keepScreenOn(true); } entryNotifier.value = entry; _overlayAnimationController = AnimationController( duration: context.read().viewerOverlayAnimation, vsync: this, ); _overlayVideoControlScale = CurvedAnimation( parent: _overlayAnimationController, // no bounce at the bottom, to avoid video controller displacement curve: Curves.easeOutQuad, ); _overlayVisible.addListener(_onOverlayVisibleChanged); _videoActionDelegate = VideoActionDelegate( collection: null, ); _viewerController = ViewerController( initialScale: const ScaleLevel(ref: ScaleReference.covered), ); initEntryControllers(entry); _onOverlayVisibleChanged(); } @override void dispose() { cleanEntryControllers(entry); _overlayVisible.dispose(); _overlayVideoControlScale.dispose(); _overlayAnimationController.dispose(); _videoActionDelegate.dispose(); _viewerController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return NotificationListener( onNotification: (dynamic notification) { if (notification is ToggleOverlayNotification) { _overlayVisible.value = notification.visible ?? !_overlayVisible.value; } else if (notification is VideoActionNotification) { _onVideoAction( context: context, entry: notification.entry, controller: notification.controller, action: notification.action, ); } return true; }, child: LayoutBuilder( builder: (context, constraints) { final viewSize = Size(constraints.maxWidth, constraints.maxHeight); return Stack( children: [ SingleEntryScroller( entry: entry, viewerController: _viewerController, ), Positioned( bottom: 0, child: _buildBottomOverlay(viewSize), ), const TopGestureAreaProtector(), const SideGestureAreaProtector(), const BottomGestureAreaProtector(), ], ); }, ), ); } Widget _buildBottomOverlay(Size viewSize) { final mainEntry = entry; final multiPageController = mainEntry.isMultiPage ? context.read().getController(mainEntry) : null; Widget? _buildExtraBottomOverlay({AvesEntry? pageEntry}) { final targetEntry = pageEntry ?? mainEntry; Widget? child; // a 360 video is both a video and a panorama but only the video controls are displayed if (targetEntry.isPureVideo) { child = Selector( selector: (context, vc) => vc.getController(targetEntry), builder: (context, videoController, child) => VideoControlOverlay( entry: targetEntry, controller: videoController, scale: _overlayVideoControlScale, onActionSelected: (action) => _onVideoAction( context: context, entry: targetEntry, controller: videoController, action: action, ), ), ); } return child != null ? ExtraBottomOverlay( viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, child: child, ) : null; } final extraBottomOverlay = mainEntry.isMultiPage ? PageEntryBuilder( multiPageController: multiPageController, builder: (pageEntry) => _buildExtraBottomOverlay(pageEntry: pageEntry) ?? const SizedBox(), ) : _buildExtraBottomOverlay(); final child = TooltipTheme( data: TooltipTheme.of(context).copyWith( preferBelow: false, ), child: Column( children: [ if (extraBottomOverlay != null) extraBottomOverlay, ViewerBottomOverlay( entries: [widget.entry], index: 0, collection: null, animationController: _overlayAnimationController, availableSize: viewSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, multiPageController: multiPageController, ), ], ), ); return ValueListenableBuilder( valueListenable: _overlayAnimationController, builder: (context, animation, child) { return Visibility( visible: !_overlayAnimationController.isDismissed, child: child!, ); }, child: child, ); } void _onVideoAction({ required BuildContext context, required AvesEntry entry, required AvesVideoController? controller, required EntryAction action, }) { if (controller != null) { _videoActionDelegate.onActionSelected(context, entry, controller, action); } } // overlay Future _onOverlayVisibleChanged({bool animate = true}) async { if (_overlayVisible.value) { await AvesApp.showSystemUI(); AvesApp.setSystemUIStyle(Theme.of(context)); if (animate) { await _overlayAnimationController.forward(); } else { _overlayAnimationController.value = _overlayAnimationController.upperBound; } } else { final mediaQuery = context.read(); setState(() { _frozenViewInsets = mediaQuery.viewInsets; _frozenViewPadding = mediaQuery.viewPadding; }); await AvesApp.hideSystemUI(); if (animate) { await _overlayAnimationController.reverse(); } else { _overlayAnimationController.reset(); } setState(() { _frozenViewInsets = null; _frozenViewPadding = null; }); } } }