#194 viewer: overlay review
This commit is contained in:
parent
ff9c652636
commit
fe7c2d61f9
34 changed files with 1048 additions and 1084 deletions
|
@ -439,8 +439,6 @@
|
|||
"settingsVideoEnableAutoPlay": "Automatische Wiedergabe",
|
||||
"settingsVideoLoopModeTile": "Schleifen-Modus",
|
||||
"settingsVideoLoopModeTitle": "Schleifen-Modus",
|
||||
"settingsVideoQuickActionsTile": "Schnelle Aktionen für Videos",
|
||||
"settingsVideoQuickActionEditorTitle": "Schnelle Aktionen",
|
||||
|
||||
"settingsSubtitleThemeTile": "Untertitel",
|
||||
"settingsSubtitleThemeTitle": "Untertitel",
|
||||
|
|
|
@ -155,7 +155,7 @@
|
|||
"videoControlsNone": "None",
|
||||
"videoControlsPlay": "Play",
|
||||
"videoControlsPlaySeek": "Play & seek",
|
||||
"videoControlsPlayOutside": "Play outside",
|
||||
"videoControlsPlayOutside": "Open with other player",
|
||||
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
|
@ -628,8 +628,6 @@
|
|||
"settingsVideoEnableAutoPlay": "Auto play",
|
||||
"settingsVideoLoopModeTile": "Loop mode",
|
||||
"settingsVideoLoopModeTitle": "Loop Mode",
|
||||
"settingsVideoQuickActionsTile": "Quick actions for videos",
|
||||
"settingsVideoQuickActionEditorTitle": "Quick Actions",
|
||||
|
||||
"settingsSubtitleThemeTile": "Subtitles",
|
||||
"settingsSubtitleThemeTitle": "Subtitles",
|
||||
|
|
|
@ -442,8 +442,6 @@
|
|||
"settingsVideoEnableAutoPlay": "Reproducción automática",
|
||||
"settingsVideoLoopModeTile": "Modo bucle",
|
||||
"settingsVideoLoopModeTitle": "Modo bucle",
|
||||
"settingsVideoQuickActionsTile": "Acciones rápidas para videos",
|
||||
"settingsVideoQuickActionEditorTitle": "Acciones rápidas",
|
||||
|
||||
"settingsSubtitleThemeTile": "Subtítulos",
|
||||
"settingsSubtitleThemeTitle": "Subtítulos",
|
||||
|
|
|
@ -440,8 +440,6 @@
|
|||
"settingsVideoEnableAutoPlay": "Lecture automatique",
|
||||
"settingsVideoLoopModeTile": "Lecture répétée",
|
||||
"settingsVideoLoopModeTitle": "Lecture répétée",
|
||||
"settingsVideoQuickActionsTile": "Actions rapides pour les vidéos",
|
||||
"settingsVideoQuickActionEditorTitle": "Actions rapides",
|
||||
|
||||
"settingsSubtitleThemeTile": "Sous-titres",
|
||||
"settingsSubtitleThemeTitle": "Sous-titres",
|
||||
|
|
|
@ -439,8 +439,6 @@
|
|||
"settingsVideoEnableAutoPlay": "Putar otomatis",
|
||||
"settingsVideoLoopModeTile": "Putar ulang",
|
||||
"settingsVideoLoopModeTitle": "Putar Ulang",
|
||||
"settingsVideoQuickActionsTile": "Aksi cepat untuk video",
|
||||
"settingsVideoQuickActionEditorTitle": "Aksi Cepat",
|
||||
|
||||
"settingsSubtitleThemeTile": "Subtitle",
|
||||
"settingsSubtitleThemeTitle": "Subtitle",
|
||||
|
|
|
@ -440,8 +440,6 @@
|
|||
"settingsVideoEnableAutoPlay": "자동 재생",
|
||||
"settingsVideoLoopModeTile": "반복 모드",
|
||||
"settingsVideoLoopModeTitle": "반복 모드",
|
||||
"settingsVideoQuickActionsTile": "동영상의 빠른 작업",
|
||||
"settingsVideoQuickActionEditorTitle": "빠른 작업",
|
||||
|
||||
"settingsSubtitleThemeTile": "자막",
|
||||
"settingsSubtitleThemeTitle": "자막",
|
||||
|
|
|
@ -440,8 +440,6 @@
|
|||
"settingsVideoEnableAutoPlay": "Reprodução automática",
|
||||
"settingsVideoLoopModeTile": "Modo de loop",
|
||||
"settingsVideoLoopModeTitle": "Modo de loop",
|
||||
"settingsVideoQuickActionsTile": "Ações rápidas para vídeos",
|
||||
"settingsVideoQuickActionEditorTitle": "Ações rápidas",
|
||||
|
||||
"settingsSubtitleThemeTile": "Legendas",
|
||||
"settingsSubtitleThemeTitle": "Legendas",
|
||||
|
|
|
@ -439,8 +439,6 @@
|
|||
"settingsVideoEnableAutoPlay": "Автозапуск воспроизведения",
|
||||
"settingsVideoLoopModeTile": "Циклический режим",
|
||||
"settingsVideoLoopModeTitle": "Цикличный режим",
|
||||
"settingsVideoQuickActionsTile": "Быстрые действия для видео",
|
||||
"settingsVideoQuickActionEditorTitle": "Быстрые действия",
|
||||
|
||||
"settingsSubtitleThemeTile": "Субтитры",
|
||||
"settingsSubtitleThemeTitle": "Субтитры",
|
||||
|
|
|
@ -21,6 +21,14 @@ enum EntryAction {
|
|||
flip,
|
||||
// vector
|
||||
viewSource,
|
||||
// video
|
||||
videoCaptureFrame,
|
||||
videoSelectStreams,
|
||||
videoSetSpeed,
|
||||
videoSettings,
|
||||
videoTogglePlay,
|
||||
videoReplay10,
|
||||
videoSkip10,
|
||||
// external
|
||||
edit,
|
||||
open,
|
||||
|
@ -41,8 +49,8 @@ class EntryActions {
|
|||
EntryAction.copy,
|
||||
EntryAction.move,
|
||||
EntryAction.toggleFavourite,
|
||||
EntryAction.viewSource,
|
||||
EntryAction.rotateScreen,
|
||||
EntryAction.viewSource,
|
||||
];
|
||||
|
||||
static const export = [
|
||||
|
@ -62,6 +70,13 @@ class EntryActions {
|
|||
];
|
||||
|
||||
static const pageActions = [
|
||||
EntryAction.videoCaptureFrame,
|
||||
EntryAction.videoSelectStreams,
|
||||
EntryAction.videoSetSpeed,
|
||||
EntryAction.videoSettings,
|
||||
EntryAction.videoTogglePlay,
|
||||
EntryAction.videoReplay10,
|
||||
EntryAction.videoSkip10,
|
||||
EntryAction.rotateCCW,
|
||||
EntryAction.rotateCW,
|
||||
EntryAction.flip,
|
||||
|
@ -72,6 +87,13 @@ class EntryActions {
|
|||
EntryAction.restore,
|
||||
EntryAction.debug,
|
||||
];
|
||||
|
||||
static const video = [
|
||||
EntryAction.videoCaptureFrame,
|
||||
EntryAction.videoSetSpeed,
|
||||
EntryAction.videoSelectStreams,
|
||||
EntryAction.videoSettings,
|
||||
];
|
||||
}
|
||||
|
||||
extension ExtraEntryAction on EntryAction {
|
||||
|
@ -110,6 +132,22 @@ extension ExtraEntryAction on EntryAction {
|
|||
// vector
|
||||
case EntryAction.viewSource:
|
||||
return context.l10n.entryActionViewSource;
|
||||
// video
|
||||
case EntryAction.videoCaptureFrame:
|
||||
return context.l10n.videoActionCaptureFrame;
|
||||
case EntryAction.videoSelectStreams:
|
||||
return context.l10n.videoActionSelectStreams;
|
||||
case EntryAction.videoSetSpeed:
|
||||
return context.l10n.videoActionSetSpeed;
|
||||
case EntryAction.videoSettings:
|
||||
return context.l10n.videoActionSettings;
|
||||
case EntryAction.videoTogglePlay:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.videoActionPlay;
|
||||
case EntryAction.videoReplay10:
|
||||
return context.l10n.videoActionReplay10;
|
||||
case EntryAction.videoSkip10:
|
||||
return context.l10n.videoActionSkip10;
|
||||
// external
|
||||
case EntryAction.edit:
|
||||
return context.l10n.entryActionEdit;
|
||||
|
@ -176,6 +214,22 @@ extension ExtraEntryAction on EntryAction {
|
|||
// vector
|
||||
case EntryAction.viewSource:
|
||||
return AIcons.vector;
|
||||
// video
|
||||
case EntryAction.videoCaptureFrame:
|
||||
return AIcons.captureFrame;
|
||||
case EntryAction.videoSelectStreams:
|
||||
return AIcons.streams;
|
||||
case EntryAction.videoSetSpeed:
|
||||
return AIcons.speed;
|
||||
case EntryAction.videoSettings:
|
||||
return AIcons.videoSettings;
|
||||
case EntryAction.videoTogglePlay:
|
||||
// different data depending on toggle state
|
||||
return AIcons.play;
|
||||
case EntryAction.videoReplay10:
|
||||
return AIcons.replay10;
|
||||
case EntryAction.videoSkip10:
|
||||
return AIcons.skip10;
|
||||
// external
|
||||
case EntryAction.edit:
|
||||
return AIcons.edit;
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum VideoAction {
|
||||
// controls
|
||||
playOutside,
|
||||
replay10,
|
||||
skip10,
|
||||
togglePlay,
|
||||
// menu
|
||||
captureFrame,
|
||||
selectStreams,
|
||||
setSpeed,
|
||||
settings,
|
||||
// TODO TLAD [video] toggle mute
|
||||
}
|
||||
|
||||
class VideoActions {
|
||||
static const menu = [
|
||||
VideoAction.captureFrame,
|
||||
VideoAction.setSpeed,
|
||||
VideoAction.selectStreams,
|
||||
VideoAction.settings,
|
||||
];
|
||||
}
|
||||
|
||||
extension ExtraVideoAction on VideoAction {
|
||||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
case VideoAction.captureFrame:
|
||||
return context.l10n.videoActionCaptureFrame;
|
||||
case VideoAction.playOutside:
|
||||
return context.l10n.entryActionOpen;
|
||||
case VideoAction.replay10:
|
||||
return context.l10n.videoActionReplay10;
|
||||
case VideoAction.skip10:
|
||||
return context.l10n.videoActionSkip10;
|
||||
case VideoAction.selectStreams:
|
||||
return context.l10n.videoActionSelectStreams;
|
||||
case VideoAction.setSpeed:
|
||||
return context.l10n.videoActionSetSpeed;
|
||||
case VideoAction.settings:
|
||||
return context.l10n.videoActionSettings;
|
||||
case VideoAction.togglePlay:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.videoActionPlay;
|
||||
}
|
||||
}
|
||||
|
||||
Widget getIcon() => Icon(getIconData());
|
||||
|
||||
IconData getIconData() {
|
||||
switch (this) {
|
||||
case VideoAction.captureFrame:
|
||||
return AIcons.captureFrame;
|
||||
case VideoAction.playOutside:
|
||||
return AIcons.openOutside;
|
||||
case VideoAction.replay10:
|
||||
return AIcons.replay10;
|
||||
case VideoAction.skip10:
|
||||
return AIcons.skip10;
|
||||
case VideoAction.selectStreams:
|
||||
return AIcons.streams;
|
||||
case VideoAction.setSpeed:
|
||||
return AIcons.speed;
|
||||
case VideoAction.settings:
|
||||
return AIcons.videoSettings;
|
||||
case VideoAction.togglePlay:
|
||||
// different data depending on toggle state
|
||||
return AIcons.play;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
|
@ -74,10 +73,6 @@ class SettingsDefaults {
|
|||
static const enableMotionPhotoAutoPlay = false;
|
||||
|
||||
// video
|
||||
static const videoQuickActions = [
|
||||
VideoAction.replay10,
|
||||
VideoAction.togglePlay,
|
||||
];
|
||||
static const enableVideoHardwareAcceleration = true;
|
||||
static const enableVideoAutoPlay = false;
|
||||
static const videoLoopMode = VideoLoopMode.shortOnly;
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:math';
|
|||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/defaults.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
|
@ -90,7 +89,6 @@ class Settings extends ChangeNotifier {
|
|||
static const imageBackgroundKey = 'image_background';
|
||||
|
||||
// video
|
||||
static const videoQuickActionsKey = 'video_quick_actions';
|
||||
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
||||
static const enableVideoAutoPlayKey = 'video_auto_play';
|
||||
static const videoLoopModeKey = 'video_loop';
|
||||
|
@ -419,10 +417,6 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// video
|
||||
|
||||
List<VideoAction> get videoQuickActions => getEnumListOrDefault(videoQuickActionsKey, SettingsDefaults.videoQuickActions, VideoAction.values);
|
||||
|
||||
set videoQuickActions(List<VideoAction> newValue) => setAndNotify(videoQuickActionsKey, newValue.map((v) => v.toString()).toList());
|
||||
|
||||
bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, SettingsDefaults.enableVideoHardwareAcceleration);
|
||||
|
||||
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
|
||||
|
@ -713,7 +707,6 @@ class Settings extends ChangeNotifier {
|
|||
case collectionBrowsingQuickActionsKey:
|
||||
case collectionSelectionQuickActionsKey:
|
||||
case viewerQuickActionsKey:
|
||||
case videoQuickActionsKey:
|
||||
if (newValue is List) {
|
||||
settingsStore.setStringList(key, newValue.cast<String>());
|
||||
} else {
|
||||
|
|
|
@ -59,7 +59,6 @@ class DebugSettingsSection extends StatelessWidget {
|
|||
'infoMapZoom': '${settings.infoMapZoom}',
|
||||
'collectionSelectionQuickActions': '${settings.collectionSelectionQuickActions}',
|
||||
'viewerQuickActions': '${settings.viewerQuickActions}',
|
||||
'videoQuickActions': '${settings.videoQuickActions}',
|
||||
'drawerTypeBookmarks': toMultiline(settings.drawerTypeBookmarks),
|
||||
'drawerAlbumBookmarks': toMultiline(settings.drawerAlbumBookmarks),
|
||||
'drawerPageBookmarks': toMultiline(settings.drawerPageBookmarks),
|
||||
|
|
|
@ -11,7 +11,6 @@ import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
|||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/video/controls.dart';
|
||||
import 'package:aves/widgets/settings/video/subtitle_theme.dart';
|
||||
import 'package:aves/widgets/settings/video/video_actions_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -37,7 +36,6 @@ class VideoSection extends StatelessWidget {
|
|||
title: Text(context.l10n.settingsVideoShowVideos),
|
||||
),
|
||||
),
|
||||
const VideoActionsTile(),
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.enableVideoHardwareAcceleration,
|
||||
builder: (context, current, child) => SwitchListTile(
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoActionsTile extends StatelessWidget {
|
||||
const VideoActionsTile({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(context.l10n.settingsVideoQuickActionsTile),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoActionEditorPage.routeName),
|
||||
builder: (context) => const VideoActionEditorPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoActionEditorPage extends StatelessWidget {
|
||||
static const routeName = '/settings/video_actions';
|
||||
|
||||
const VideoActionEditorPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuickActionEditorPage<VideoAction>(
|
||||
title: context.l10n.settingsVideoQuickActionEditorTitle,
|
||||
bannerText: context.l10n.settingsViewerQuickActionEditorBanner,
|
||||
allAvailableActions: VideoActions.menu,
|
||||
actionIcon: (action) => action.getIcon(),
|
||||
actionText: (context, action) => action.getText(context),
|
||||
load: () => settings.videoQuickActions,
|
||||
save: (actions) => settings.videoQuickActions = actions,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -46,14 +46,6 @@ class ViewerOverlayPage extends StatelessWidget {
|
|||
title: Text(context.l10n.settingsViewerShowOverlayOnOpening),
|
||||
),
|
||||
),
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.showOverlayMinimap,
|
||||
builder: (context, current, child) => SwitchListTile(
|
||||
value: current,
|
||||
onChanged: (v) => settings.showOverlayMinimap = v,
|
||||
title: Text(context.l10n.settingsViewerShowMinimap),
|
||||
),
|
||||
),
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.showOverlayInfo,
|
||||
builder: (context, current, child) => SwitchListTile(
|
||||
|
@ -75,6 +67,14 @@ class ViewerOverlayPage extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.showOverlayMinimap,
|
||||
builder: (context, current, child) => SwitchListTile(
|
||||
value: current,
|
||||
onChanged: (v) => settings.showOverlayMinimap = v,
|
||||
title: Text(context.l10n.settingsViewerShowMinimap),
|
||||
),
|
||||
),
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.showOverlayThumbnailPreview,
|
||||
builder: (context, current, child) => SwitchListTile(
|
||||
|
|
|
@ -30,7 +30,18 @@ class ViewerActionEditorPage extends StatelessWidget {
|
|||
const ViewerActionEditorPage({Key? key}) : super(key: key);
|
||||
|
||||
static const allAvailableActions = [
|
||||
...EntryActions.topLevel,
|
||||
EntryAction.share,
|
||||
EntryAction.edit,
|
||||
EntryAction.rename,
|
||||
EntryAction.delete,
|
||||
EntryAction.copy,
|
||||
EntryAction.move,
|
||||
EntryAction.toggleFavourite,
|
||||
EntryAction.rotateScreen,
|
||||
EntryAction.videoCaptureFrame,
|
||||
EntryAction.videoSetSpeed,
|
||||
EntryAction.videoSelectStreams,
|
||||
EntryAction.viewSource,
|
||||
EntryAction.rotateCCW,
|
||||
EntryAction.rotateCW,
|
||||
EntryAction.flip,
|
||||
|
|
|
@ -34,7 +34,9 @@ import 'package:aves/widgets/viewer/action/printer.dart';
|
|||
import 'package:aves/widgets/viewer/action/single_entry_editor.dart';
|
||||
import 'package:aves/widgets/viewer/debug/debug_page.dart';
|
||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/source_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -99,7 +101,19 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
case EntryAction.viewSource:
|
||||
_goToSourceViewer(context);
|
||||
break;
|
||||
// external
|
||||
// video
|
||||
case EntryAction.videoCaptureFrame:
|
||||
case EntryAction.videoSelectStreams:
|
||||
case EntryAction.videoSetSpeed:
|
||||
case EntryAction.videoSettings:
|
||||
case EntryAction.videoTogglePlay:
|
||||
case EntryAction.videoReplay10:
|
||||
case EntryAction.videoSkip10:
|
||||
final controller = context.read<VideoConductor>().getController(entry);
|
||||
if (controller != null) {
|
||||
VideoActionNotification(controller: controller, action: action).dispatch(context);
|
||||
}
|
||||
break;
|
||||
case EntryAction.edit:
|
||||
androidAppService.edit(entry.uri, entry.mimeType).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
|
|
|
@ -14,17 +14,16 @@ import 'package:aves/utils/change_notifier.dart';
|
|||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
import 'package:aves/widgets/common/basic/insets.dart';
|
||||
import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart';
|
||||
import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
|
||||
import 'package:aves/widgets/viewer/hero.dart';
|
||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/common.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/panorama.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/video/video.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/panorama.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/top.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/video.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
|
@ -60,8 +59,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
final AChangeNotifier _verticalScrollNotifier = AChangeNotifier();
|
||||
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
|
||||
late AnimationController _overlayAnimationController;
|
||||
late Animation<double> _topOverlayScale, _bottomOverlayScale;
|
||||
late Animation<Offset> _bottomOverlayOffset;
|
||||
late Animation<double> _overlayButtonScale, _overlayVideoControlScale;
|
||||
late Animation<Offset> _overlayTopOffset;
|
||||
EdgeInsets? _frozenViewInsets, _frozenViewPadding;
|
||||
late VideoActionDelegate _videoActionDelegate;
|
||||
final Map<MultiPageController, Future<void> Function()> _multiPageControllerPageListeners = {};
|
||||
|
@ -110,17 +109,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_topOverlayScale = CurvedAnimation(
|
||||
_overlayButtonScale = CurvedAnimation(
|
||||
parent: _overlayAnimationController,
|
||||
// a little bounce at the top
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
_bottomOverlayScale = CurvedAnimation(
|
||||
_overlayVideoControlScale = CurvedAnimation(
|
||||
parent: _overlayAnimationController,
|
||||
// no bounce at the bottom, to avoid video controller displacement
|
||||
curve: Curves.easeOutQuad,
|
||||
);
|
||||
_bottomOverlayOffset = Tween(begin: const Offset(0, 1), end: const Offset(0, 0)).animate(CurvedAnimation(
|
||||
_overlayTopOffset = Tween(begin: const Offset(0, -1), end: const Offset(0, 0)).animate(CurvedAnimation(
|
||||
parent: _overlayAnimationController,
|
||||
curve: Curves.easeOutQuad,
|
||||
));
|
||||
|
@ -209,7 +208,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
if (_currentHorizontalPage != index) {
|
||||
_horizontalPager.jumpToPage(index);
|
||||
}
|
||||
} else if (notification is VideoGestureNotification) {
|
||||
} else if (notification is VideoActionNotification) {
|
||||
final controller = notification.controller;
|
||||
final action = notification.action;
|
||||
_videoActionDelegate.onActionSelected(context, controller, action);
|
||||
|
@ -245,27 +244,20 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
Widget child = ValueListenableBuilder<AvesEntry?>(
|
||||
valueListenable: _entryNotifier,
|
||||
builder: (context, mainEntry, child) {
|
||||
if (mainEntry == null) return const SizedBox.shrink();
|
||||
if (mainEntry == null) return const SizedBox();
|
||||
|
||||
Widget _buildContent({AvesEntry? pageEntry}) {
|
||||
return EmbeddedDataOpener(
|
||||
entry: mainEntry,
|
||||
child: ViewerTopOverlay(
|
||||
mainEntry: mainEntry,
|
||||
scale: _topOverlayScale,
|
||||
canToggleFavourite: hasCollection,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildContent();
|
||||
return SlideTransition(
|
||||
position: _overlayTopOffset,
|
||||
child: ViewerTopOverlay(
|
||||
entries: entries,
|
||||
index: _currentHorizontalPage,
|
||||
hasCollection: hasCollection,
|
||||
mainEntry: mainEntry,
|
||||
scale: _overlayButtonScale,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -298,7 +290,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
Widget child = ValueListenableBuilder<AvesEntry?>(
|
||||
valueListenable: _entryNotifier,
|
||||
builder: (context, mainEntry, child) {
|
||||
if (mainEntry == null) return const SizedBox.shrink();
|
||||
if (mainEntry == null) return const SizedBox();
|
||||
|
||||
final multiPageController = mainEntry.isMultiPage ? context.read<MultiPageConductor>().getController(mainEntry) : null;
|
||||
|
||||
Widget? _buildExtraBottomOverlay({AvesEntry? pageEntry}) {
|
||||
|
@ -311,7 +304,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
builder: (context, videoController, child) => VideoControlOverlay(
|
||||
entry: targetEntry,
|
||||
controller: videoController,
|
||||
scale: _bottomOverlayScale,
|
||||
scale: _overlayVideoControlScale,
|
||||
onActionSelected: (action) {
|
||||
if (videoController != null) {
|
||||
_videoActionDelegate.onActionSelected(context, videoController, action);
|
||||
|
@ -329,7 +322,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
} else if (targetEntry.is360) {
|
||||
child = PanoramaOverlay(
|
||||
entry: targetEntry,
|
||||
scale: _bottomOverlayScale,
|
||||
scale: _overlayButtonScale,
|
||||
);
|
||||
}
|
||||
return child != null
|
||||
|
@ -348,21 +341,24 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
)
|
||||
: _buildExtraBottomOverlay();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (extraBottomOverlay != null) extraBottomOverlay,
|
||||
SlideTransition(
|
||||
position: _bottomOverlayOffset,
|
||||
child: ViewerBottomOverlay(
|
||||
return TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (extraBottomOverlay != null) extraBottomOverlay,
|
||||
ViewerBottomOverlay(
|
||||
entries: entries,
|
||||
index: _currentHorizontalPage,
|
||||
showPosition: hasCollection,
|
||||
hasCollection: hasCollection,
|
||||
animationController: _overlayAnimationController,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
multiPageController: multiPageController,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
210
lib/widgets/viewer/overlay/bottom.dart
Normal file
210
lib/widgets/viewer/overlay/bottom.dart
Normal file
|
@ -0,0 +1,210 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/multipage.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/viewer_button_row.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ViewerBottomOverlay extends StatefulWidget {
|
||||
final List<AvesEntry> entries;
|
||||
final int index;
|
||||
final bool hasCollection;
|
||||
final AnimationController animationController;
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final MultiPageController? multiPageController;
|
||||
|
||||
const ViewerBottomOverlay({
|
||||
Key? key,
|
||||
required this.entries,
|
||||
required this.index,
|
||||
required this.hasCollection,
|
||||
required this.animationController,
|
||||
this.viewInsets,
|
||||
this.viewPadding,
|
||||
required this.multiPageController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ViewerBottomOverlayState();
|
||||
}
|
||||
|
||||
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,
|
||||
hasCollection: widget.hasCollection,
|
||||
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 bool hasCollection;
|
||||
final MultiPageController? multiPageController;
|
||||
final AnimationController animationController;
|
||||
|
||||
const _BottomOverlayContent({
|
||||
Key? key,
|
||||
required this.entries,
|
||||
required this.index,
|
||||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
required this.hasCollection,
|
||||
required this.multiPageController,
|
||||
required this.animationController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_BottomOverlayContent> createState() => _BottomOverlayContentState();
|
||||
}
|
||||
|
||||
class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||
late Animation<double> _buttonScale, _thumbnailOpacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final animationController = widget.animationController;
|
||||
_buttonScale = CurvedAnimation(
|
||||
parent: animationController,
|
||||
// a little bounce at the top
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
_thumbnailOpacity = CurvedAnimation(
|
||||
parent: animationController,
|
||||
curve: Curves.easeOutQuad,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mainEntry = widget.mainEntry;
|
||||
final pageEntry = widget.pageEntry;
|
||||
final multiPageController = widget.multiPageController;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
mainEntry.metadataChangeNotifier,
|
||||
pageEntry.metadataChangeNotifier,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
return Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.size.width,
|
||||
builder: (context, mqWidth, child) {
|
||||
return SizedBox(
|
||||
width: mqWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mainEntry.isMultiPage && multiPageController != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: FadeTransition(
|
||||
opacity: _thumbnailOpacity,
|
||||
child: MultiPageOverlay(
|
||||
controller: multiPageController,
|
||||
availableWidth: mqWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
ViewerButtonRow(
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry,
|
||||
scale: _buttonScale,
|
||||
canToggleFavourite: widget.hasCollection,
|
||||
),
|
||||
if (settings.showOverlayThumbnailPreview)
|
||||
FadeTransition(
|
||||
opacity: _thumbnailOpacity,
|
||||
child: ViewerThumbnailPreview(
|
||||
availableWidth: mqWidth,
|
||||
displayedIndex: widget.index,
|
||||
entries: widget.entries,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraBottomOverlay extends StatelessWidget {
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final Widget child;
|
||||
|
||||
const ExtraBottomOverlay({
|
||||
Key? key,
|
||||
this.viewInsets,
|
||||
this.viewPadding,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mq = context.select<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>((mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding));
|
||||
final mqWidth = mq.item1;
|
||||
final mqViewInsets = mq.item2;
|
||||
final mqViewPadding = mq.item3;
|
||||
|
||||
final viewInsets = this.viewInsets ?? mqViewInsets;
|
||||
final viewPadding = this.viewPadding ?? mqViewPadding;
|
||||
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + const EdgeInsets.symmetric(horizontal: 8.0);
|
||||
|
||||
return Padding(
|
||||
padding: safePadding,
|
||||
child: SizedBox(
|
||||
width: mqWidth - safePadding.horizontal,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,232 +0,0 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/metadata/overlay.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/details.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/multipage.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/thumbnail_preview.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ViewerBottomOverlay extends StatefulWidget {
|
||||
final List<AvesEntry> entries;
|
||||
final int index;
|
||||
final bool showPosition;
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final MultiPageController? multiPageController;
|
||||
|
||||
const ViewerBottomOverlay({
|
||||
Key? key,
|
||||
required this.entries,
|
||||
required this.index,
|
||||
required this.showPosition,
|
||||
this.viewInsets,
|
||||
this.viewPadding,
|
||||
required this.multiPageController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ViewerBottomOverlayState();
|
||||
}
|
||||
|
||||
class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
||||
late Future<OverlayMetadata?> _detailLoader;
|
||||
AvesEntry? _lastEntry;
|
||||
OverlayMetadata? _lastDetails;
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initDetailLoader();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ViewerBottomOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (entry != _lastEntry) {
|
||||
_initDetailLoader();
|
||||
}
|
||||
}
|
||||
|
||||
void _initDetailLoader() {
|
||||
final requestEntry = entry;
|
||||
_detailLoader = requestEntry != null ? metadataFetchService.getOverlayMetadata(requestEntry) : SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasEdgeContent = settings.showOverlayThumbnailPreview || settings.showOverlayInfo || multiPageController != null;
|
||||
final blurred = settings.enableOverlayBlurEffect;
|
||||
return BlurredRect(
|
||||
enabled: hasEdgeContent && blurred,
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom),
|
||||
builder: (context, mqPaddingBottom, child) {
|
||||
return Container(
|
||||
color: hasEdgeContent ? overlayBackgroundColor(blurred: blurred) : Colors.transparent,
|
||||
padding: EdgeInsets.only(bottom: mqPaddingBottom),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: FutureBuilder<OverlayMetadata?>(
|
||||
future: _detailLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||
_lastDetails = snapshot.data;
|
||||
_lastEntry = entry;
|
||||
}
|
||||
if (_lastEntry == null) return const SizedBox();
|
||||
final mainEntry = _lastEntry!;
|
||||
|
||||
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
|
||||
entries: entries,
|
||||
index: widget.index,
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
details: _lastDetails,
|
||||
showPosition: widget.showPosition,
|
||||
multiPageController: multiPageController,
|
||||
);
|
||||
|
||||
return multiPageController != null
|
||||
? PageEntryBuilder(
|
||||
multiPageController: multiPageController!,
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildContent();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomOverlayContent extends AnimatedWidget {
|
||||
final List<AvesEntry> entries;
|
||||
final int index;
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
final OverlayMetadata? details;
|
||||
final bool showPosition;
|
||||
final MultiPageController? multiPageController;
|
||||
|
||||
_BottomOverlayContent({
|
||||
Key? key,
|
||||
required this.entries,
|
||||
required this.index,
|
||||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
required this.details,
|
||||
required this.showPosition,
|
||||
required this.multiPageController,
|
||||
}) : super(
|
||||
key: key,
|
||||
listenable: Listenable.merge([
|
||||
mainEntry.metadataChangeNotifier,
|
||||
pageEntry.metadataChangeNotifier,
|
||||
]),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyText2!.copyWith(
|
||||
shadows: Constants.embossShadows,
|
||||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.size.width,
|
||||
builder: (context, mqWidth, child) {
|
||||
return SizedBox(
|
||||
width: mqWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mainEntry.isMultiPage && multiPageController != null)
|
||||
MultiPageOverlay(
|
||||
controller: multiPageController!,
|
||||
availableWidth: mqWidth,
|
||||
),
|
||||
if (settings.showOverlayInfo)
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return ViewerDetailOverlay(
|
||||
pageEntry: pageEntry,
|
||||
details: details,
|
||||
position: showPosition ? '${index + 1}/${entries.length}' : null,
|
||||
availableWidth: constraints.maxWidth,
|
||||
multiPageController: multiPageController,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (settings.showOverlayThumbnailPreview)
|
||||
ViewerThumbnailPreview(
|
||||
availableWidth: mqWidth,
|
||||
displayedIndex: index,
|
||||
entries: entries,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraBottomOverlay extends StatelessWidget {
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final Widget child;
|
||||
|
||||
const ExtraBottomOverlay({
|
||||
Key? key,
|
||||
this.viewInsets,
|
||||
this.viewPadding,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mq = context.select<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>((mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding));
|
||||
final mqWidth = mq.item1;
|
||||
final mqViewInsets = mq.item2;
|
||||
final mqViewPadding = mq.item3;
|
||||
|
||||
final viewInsets = this.viewInsets ?? mqViewInsets;
|
||||
final viewPadding = this.viewPadding ?? mqViewPadding;
|
||||
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + const EdgeInsets.symmetric(horizontal: 8.0);
|
||||
|
||||
return Padding(
|
||||
padding: safePadding,
|
||||
child: SizedBox(
|
||||
width: mqWidth - safePadding.horizontal,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,281 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/video/controls.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/video/progress_bar.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VideoControlOverlay extends StatefulWidget {
|
||||
final AvesEntry entry;
|
||||
final AvesVideoController? controller;
|
||||
final Animation<double> scale;
|
||||
final Function(VideoAction value) onActionSelected;
|
||||
final VoidCallback onActionMenuOpened;
|
||||
|
||||
const VideoControlOverlay({
|
||||
Key? key,
|
||||
required this.entry,
|
||||
required this.controller,
|
||||
required this.scale,
|
||||
required this.onActionSelected,
|
||||
required this.onActionMenuOpened,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VideoControlOverlayState();
|
||||
}
|
||||
|
||||
class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTickerProviderStateMixin {
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
||||
Animation<double> get scale => widget.scale;
|
||||
|
||||
AvesVideoController? get controller => widget.controller;
|
||||
|
||||
Stream<VideoStatus> get statusStream => controller?.statusStream ?? Stream.value(VideoStatus.idle);
|
||||
|
||||
Stream<int> get positionStream => controller?.positionStream ?? Stream.value(0);
|
||||
|
||||
bool get isPlaying => controller?.isPlaying ?? false;
|
||||
|
||||
static const double outerPadding = 8;
|
||||
static const double innerPadding = 8;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<VideoStatus>(
|
||||
stream: statusStream,
|
||||
builder: (context, snapshot) {
|
||||
// do not use stream snapshot because it is obsolete when switching between videos
|
||||
final status = controller?.status ?? VideoStatus.idle;
|
||||
Widget child;
|
||||
if (status == VideoStatus.error) {
|
||||
const action = VideoAction.playOutside;
|
||||
child = Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: OverlayButton(
|
||||
scale: scale,
|
||||
child: IconButton(
|
||||
icon: action.getIcon(),
|
||||
onPressed: entry.trashed ? null : () => widget.onActionSelected(action),
|
||||
tooltip: action.getText(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
child = Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.size.width - mq.padding.horizontal,
|
||||
builder: (context, mqWidth, child) {
|
||||
final buttonWidth = OverlayButton.getSize(context);
|
||||
final availableCount = ((mqWidth - outerPadding * 2) / (buttonWidth + innerPadding)).floor();
|
||||
return Selector<Settings, List<VideoAction>>(
|
||||
selector: (context, s) => s.videoQuickActions,
|
||||
builder: (context, videoQuickActions, child) {
|
||||
final quickActions = videoQuickActions.take(availableCount - 1).toList();
|
||||
final menuActions = VideoActions.menu.where((action) => !quickActions.contains(action)).toList();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_ButtonRow(
|
||||
quickActions: quickActions,
|
||||
menuActions: menuActions,
|
||||
scale: scale,
|
||||
controller: controller,
|
||||
onActionSelected: widget.onActionSelected,
|
||||
onActionMenuOpened: widget.onActionMenuOpened,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: VideoProgressBar(
|
||||
controller: controller,
|
||||
scale: scale,
|
||||
),
|
||||
),
|
||||
VideoControlRow(
|
||||
controller: controller,
|
||||
scale: scale,
|
||||
onActionSelected: widget.onActionSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ButtonRow extends StatelessWidget {
|
||||
final List<VideoAction> quickActions, menuActions;
|
||||
final Animation<double> scale;
|
||||
final AvesVideoController? controller;
|
||||
final Function(VideoAction value) onActionSelected;
|
||||
final VoidCallback onActionMenuOpened;
|
||||
|
||||
const _ButtonRow({
|
||||
Key? key,
|
||||
required this.quickActions,
|
||||
required this.menuActions,
|
||||
required this.scale,
|
||||
required this.controller,
|
||||
required this.onActionSelected,
|
||||
required this.onActionMenuOpened,
|
||||
}) : super(key: key);
|
||||
|
||||
static const double padding = 8;
|
||||
|
||||
bool get isPlaying => controller?.isPlaying ?? false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...quickActions.map((action) => _buildOverlayButton(context, action)),
|
||||
if (menuActions.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: padding),
|
||||
child: OverlayButton(
|
||||
scale: scale,
|
||||
child: MenuIconTheme(
|
||||
child: AvesPopupMenuButton<VideoAction>(
|
||||
itemBuilder: (context) => menuActions.map((action) => _buildPopupMenuItem(context, action)).toList(),
|
||||
onSelected: (action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||
onActionSelected(action);
|
||||
},
|
||||
onMenuOpened: onActionMenuOpened,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverlayButton(BuildContext context, VideoAction action) {
|
||||
late Widget child;
|
||||
void onPressed() => onActionSelected(action);
|
||||
|
||||
ValueListenableBuilder<bool> _buildFromListenable(ValueListenable<bool>? enabledNotifier) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: enabledNotifier ?? ValueNotifier(false),
|
||||
builder: (context, canDo, child) => IconButton(
|
||||
icon: child!,
|
||||
onPressed: canDo ? onPressed : null,
|
||||
tooltip: action.getText(context),
|
||||
),
|
||||
child: action.getIcon(),
|
||||
);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case VideoAction.captureFrame:
|
||||
child = _buildFromListenable(controller?.canCaptureFrameNotifier);
|
||||
break;
|
||||
case VideoAction.selectStreams:
|
||||
child = _buildFromListenable(controller?.canSelectStreamNotifier);
|
||||
break;
|
||||
case VideoAction.setSpeed:
|
||||
child = _buildFromListenable(controller?.canSetSpeedNotifier);
|
||||
break;
|
||||
case VideoAction.togglePlay:
|
||||
child = PlayToggler(
|
||||
controller: controller,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
case VideoAction.playOutside:
|
||||
case VideoAction.replay10:
|
||||
case VideoAction.skip10:
|
||||
case VideoAction.settings:
|
||||
child = IconButton(
|
||||
icon: action.getIcon(),
|
||||
onPressed: onPressed,
|
||||
tooltip: action.getText(context),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: padding),
|
||||
child: OverlayButton(
|
||||
scale: scale,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuEntry<VideoAction> _buildPopupMenuItem(BuildContext context, VideoAction action) {
|
||||
late final bool enabled;
|
||||
switch (action) {
|
||||
case VideoAction.captureFrame:
|
||||
enabled = controller?.canCaptureFrameNotifier.value ?? false;
|
||||
break;
|
||||
case VideoAction.selectStreams:
|
||||
enabled = controller?.canSelectStreamNotifier.value ?? false;
|
||||
break;
|
||||
case VideoAction.setSpeed:
|
||||
enabled = controller?.canSetSpeedNotifier.value ?? false;
|
||||
break;
|
||||
case VideoAction.replay10:
|
||||
case VideoAction.skip10:
|
||||
case VideoAction.settings:
|
||||
case VideoAction.togglePlay:
|
||||
enabled = true;
|
||||
break;
|
||||
case VideoAction.playOutside:
|
||||
enabled = !(controller?.entry.trashed ?? true);
|
||||
break;
|
||||
}
|
||||
|
||||
Widget? child;
|
||||
switch (action) {
|
||||
case VideoAction.togglePlay:
|
||||
child = PlayToggler(
|
||||
controller: controller,
|
||||
isMenuItem: true,
|
||||
);
|
||||
break;
|
||||
case VideoAction.captureFrame:
|
||||
case VideoAction.playOutside:
|
||||
case VideoAction.replay10:
|
||||
case VideoAction.skip10:
|
||||
case VideoAction.selectStreams:
|
||||
case VideoAction.setSpeed:
|
||||
case VideoAction.settings:
|
||||
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
||||
break;
|
||||
}
|
||||
|
||||
return PopupMenuItem(
|
||||
value: action,
|
||||
enabled: enabled,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,13 +5,16 @@ import 'package:aves/model/metadata/overlay.dart';
|
|||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/format.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -21,7 +24,98 @@ const double _iconSize = 16.0;
|
|||
const double _interRowPadding = 2.0;
|
||||
const double _subRowMinWidth = 300.0;
|
||||
|
||||
class ViewerDetailOverlay extends StatelessWidget {
|
||||
class ViewerDetailOverlay extends StatefulWidget {
|
||||
final List<AvesEntry> entries;
|
||||
final int index;
|
||||
final bool hasCollection;
|
||||
final MultiPageController? multiPageController;
|
||||
|
||||
const ViewerDetailOverlay({
|
||||
Key? key,
|
||||
required this.entries,
|
||||
required this.index,
|
||||
required this.hasCollection,
|
||||
required this.multiPageController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ViewerDetailOverlay> createState() => _ViewerDetailOverlayState();
|
||||
}
|
||||
|
||||
class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||
List<AvesEntry> get entries => widget.entries;
|
||||
|
||||
AvesEntry? get entry {
|
||||
final index = widget.index;
|
||||
return index < entries.length ? entries[index] : null;
|
||||
}
|
||||
|
||||
late Future<OverlayMetadata?> _detailLoader;
|
||||
AvesEntry? _lastEntry;
|
||||
OverlayMetadata? _lastDetails;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initDetailLoader();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ViewerDetailOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (entry != _lastEntry) {
|
||||
_initDetailLoader();
|
||||
}
|
||||
}
|
||||
|
||||
void _initDetailLoader() {
|
||||
final requestEntry = entry;
|
||||
_detailLoader = requestEntry != null ? metadataFetchService.getOverlayMetadata(requestEntry) : SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
|
||||
return FutureBuilder<OverlayMetadata?>(
|
||||
future: _detailLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||
_lastDetails = snapshot.data;
|
||||
_lastEntry = entry;
|
||||
}
|
||||
if (_lastEntry == null) return const SizedBox();
|
||||
final mainEntry = _lastEntry!;
|
||||
|
||||
final multiPageController = widget.multiPageController;
|
||||
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
details: _lastDetails,
|
||||
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
|
||||
availableWidth: availableWidth,
|
||||
multiPageController: multiPageController,
|
||||
);
|
||||
|
||||
return multiPageController != null
|
||||
? PageEntryBuilder(
|
||||
multiPageController: multiPageController,
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildContent();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewerDetailOverlayContent extends StatelessWidget {
|
||||
final AvesEntry pageEntry;
|
||||
final OverlayMetadata? details;
|
||||
final String? position;
|
||||
|
@ -30,7 +124,7 @@ class ViewerDetailOverlay extends StatelessWidget {
|
|||
|
||||
static const padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
|
||||
|
||||
const ViewerDetailOverlay({
|
||||
const ViewerDetailOverlayContent({
|
||||
Key? key,
|
||||
required this.pageEntry,
|
||||
required this.details,
|
||||
|
@ -46,49 +140,57 @@ class ViewerDetailOverlay extends StatelessWidget {
|
|||
final hasShootingDetails = details != null && !details!.isEmpty && settings.showOverlayShootingDetails;
|
||||
final animationDuration = context.select<DurationsData, Duration>((v) => v.viewerOverlayChangeAnimation);
|
||||
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Selector<MediaQueryData, Orientation>(
|
||||
selector: (context, mq) => mq.orientation,
|
||||
builder: (context, orientation, child) {
|
||||
final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth;
|
||||
final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth;
|
||||
return DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyText2!.copyWith(
|
||||
shadows: Constants.embossShadows,
|
||||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Selector<MediaQueryData, Orientation>(
|
||||
selector: (context, mq) => mq.orientation,
|
||||
builder: (context, orientation, child) {
|
||||
final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth;
|
||||
final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (positionTitle.isNotEmpty) positionTitle,
|
||||
_buildSoloLocationRow(animationDuration),
|
||||
if (twoColumns)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: subRowWidth,
|
||||
child: _DateRow(
|
||||
entry: pageEntry,
|
||||
multiPageController: multiPageController,
|
||||
)),
|
||||
_buildDuoShootingRow(subRowWidth, hasShootingDetails, animationDuration),
|
||||
],
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (positionTitle.isNotEmpty) positionTitle,
|
||||
if (twoColumns)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: subRowWidth,
|
||||
child: _DateRow(
|
||||
entry: pageEntry,
|
||||
multiPageController: multiPageController,
|
||||
)),
|
||||
_buildDuoShootingRow(subRowWidth, hasShootingDetails, animationDuration),
|
||||
],
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||
width: subRowWidth,
|
||||
child: _DateRow(
|
||||
entry: pageEntry,
|
||||
multiPageController: multiPageController,
|
||||
),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||
width: subRowWidth,
|
||||
child: _DateRow(
|
||||
entry: pageEntry,
|
||||
multiPageController: multiPageController,
|
||||
),
|
||||
),
|
||||
_buildSoloShootingRow(subRowWidth, hasShootingDetails, animationDuration),
|
||||
_buildSoloShootingRow(subRowWidth, hasShootingDetails, animationDuration),
|
||||
],
|
||||
_buildSoloLocationRow(animationDuration),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -17,11 +17,11 @@ class ViewEntryNotification extends Notification {
|
|||
}
|
||||
|
||||
@immutable
|
||||
class VideoGestureNotification extends Notification {
|
||||
class VideoActionNotification extends Notification {
|
||||
final AvesVideoController controller;
|
||||
final VideoAction action;
|
||||
final EntryAction action;
|
||||
|
||||
const VideoGestureNotification({
|
||||
const VideoActionNotification({
|
||||
required this.controller,
|
||||
required this.action,
|
||||
});
|
||||
|
|
|
@ -1,329 +1,106 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/favourite_toggler.dart';
|
||||
import 'package:aves/widgets/viewer/action/entry_action_delegate.dart';
|
||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/details.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/minimap.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:aves/widgets/viewer/visual/conductor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ViewerTopOverlay extends StatelessWidget {
|
||||
final List<AvesEntry> entries;
|
||||
final int index;
|
||||
final AvesEntry mainEntry;
|
||||
final Animation<double> scale;
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final bool canToggleFavourite;
|
||||
|
||||
static const double outerPadding = 8;
|
||||
static const double innerPadding = 8;
|
||||
final bool hasCollection;
|
||||
|
||||
const ViewerTopOverlay({
|
||||
Key? key,
|
||||
required this.entries,
|
||||
required this.index,
|
||||
required this.mainEntry,
|
||||
required this.scale,
|
||||
required this.canToggleFavourite,
|
||||
required this.hasCollection,
|
||||
required this.viewInsets,
|
||||
required this.viewPadding,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(outerPadding),
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.size.width - mq.padding.horizontal,
|
||||
builder: (context, mqWidth, child) {
|
||||
final buttonWidth = OverlayButton.getSize(context);
|
||||
final availableCount = ((mqWidth - outerPadding * 2 - buttonWidth) / (buttonWidth + innerPadding)).floor();
|
||||
late Widget child;
|
||||
|
||||
return mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildOverlay(context, availableCount, mainEntry, pageEntry: pageEntry),
|
||||
)
|
||||
: _buildOverlay(context, availableCount, mainEntry);
|
||||
},
|
||||
if (mainEntry.isMultiPage) {
|
||||
final multiPageController = context.read<MultiPageConductor>().getController(mainEntry);
|
||||
child = PageEntryBuilder(
|
||||
multiPageController: multiPageController,
|
||||
builder: (pageEntry) => _buildOverlay(
|
||||
context,
|
||||
mainEntry,
|
||||
pageEntry: pageEntry,
|
||||
multiPageController: multiPageController,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverlay(BuildContext context, int availableCount, AvesEntry mainEntry, {AvesEntry? pageEntry}) {
|
||||
pageEntry ??= mainEntry;
|
||||
final trashed = mainEntry.trashed;
|
||||
|
||||
bool _isVisible(EntryAction action) {
|
||||
if (trashed) {
|
||||
switch (action) {
|
||||
case EntryAction.delete:
|
||||
case EntryAction.restore:
|
||||
return true;
|
||||
case EntryAction.debug:
|
||||
return kDebugMode;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry! : mainEntry;
|
||||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
return canToggleFavourite;
|
||||
case EntryAction.delete:
|
||||
case EntryAction.rename:
|
||||
case EntryAction.copy:
|
||||
case EntryAction.move:
|
||||
return targetEntry.canEdit;
|
||||
case EntryAction.rotateCCW:
|
||||
case EntryAction.rotateCW:
|
||||
case EntryAction.flip:
|
||||
return targetEntry.canRotateAndFlip;
|
||||
case EntryAction.convert:
|
||||
case EntryAction.print:
|
||||
return !targetEntry.isVideo && device.canPrint;
|
||||
case EntryAction.openMap:
|
||||
return targetEntry.hasGps;
|
||||
case EntryAction.viewSource:
|
||||
return targetEntry.isSvg;
|
||||
case EntryAction.rotateScreen:
|
||||
return settings.isRotationLocked;
|
||||
case EntryAction.addShortcut:
|
||||
return device.canPinShortcut;
|
||||
case EntryAction.copyToClipboard:
|
||||
case EntryAction.edit:
|
||||
case EntryAction.open:
|
||||
case EntryAction.setAs:
|
||||
case EntryAction.share:
|
||||
return true;
|
||||
case EntryAction.restore:
|
||||
return false;
|
||||
case EntryAction.debug:
|
||||
return kDebugMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final buttonRow = Selector<Settings, bool>(
|
||||
selector: (context, s) => s.isRotationLocked,
|
||||
builder: (context, s, child) {
|
||||
final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(_isVisible).take(availableCount - 1).toList();
|
||||
final topLevelActions = EntryActions.topLevel.where((action) => !quickActions.contains(action)).where(_isVisible).toList();
|
||||
final exportActions = EntryActions.export.where((action) => !quickActions.contains(action)).where(_isVisible).toList();
|
||||
return _TopOverlayRow(
|
||||
quickActions: quickActions,
|
||||
topLevelActions: topLevelActions,
|
||||
exportActions: exportActions,
|
||||
scale: scale,
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (settings.showOverlayMinimap) {
|
||||
final viewStateConductor = context.read<ViewStateConductor>();
|
||||
final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buttonRow,
|
||||
const SizedBox(height: 8),
|
||||
FadeTransition(
|
||||
opacity: scale,
|
||||
child: Minimap(
|
||||
viewStateNotifier: viewStateNotifier,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
child = _buildOverlay(context, mainEntry);
|
||||
}
|
||||
return buttonRow;
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _TopOverlayRow extends StatelessWidget {
|
||||
final List<EntryAction> quickActions, topLevelActions, exportActions;
|
||||
final Animation<double> scale;
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
Widget _buildOverlay(
|
||||
BuildContext context,
|
||||
AvesEntry mainEntry, {
|
||||
AvesEntry? pageEntry,
|
||||
MultiPageController? multiPageController,
|
||||
}) {
|
||||
pageEntry ??= mainEntry;
|
||||
|
||||
AvesEntry get favouriteTargetEntry => mainEntry.isBurst ? pageEntry : mainEntry;
|
||||
final showInfo = settings.showOverlayInfo;
|
||||
|
||||
const _TopOverlayRow({
|
||||
Key? key,
|
||||
required this.quickActions,
|
||||
required this.topLevelActions,
|
||||
required this.exportActions,
|
||||
required this.scale,
|
||||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
}) : super(key: key);
|
||||
final viewStateConductor = context.read<ViewStateConductor>();
|
||||
final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasOverflowMenu = pageEntry.canRotateAndFlip || topLevelActions.isNotEmpty || exportActions.isNotEmpty;
|
||||
return Row(
|
||||
final blurred = settings.enableOverlayBlurEffect;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
OverlayButton(
|
||||
scale: scale,
|
||||
child: Navigator.canPop(context) ? const BackButton() : const CloseButton(),
|
||||
),
|
||||
const Spacer(),
|
||||
...quickActions.map((action) => _buildOverlayButton(context, action)),
|
||||
if (hasOverflowMenu)
|
||||
OverlayButton(
|
||||
scale: scale,
|
||||
child: MenuIconTheme(
|
||||
child: AvesPopupMenuButton<EntryAction>(
|
||||
key: const Key('entry-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
final exportInternalActions = exportActions.whereNot(EntryActions.exportExternal.contains).toList();
|
||||
final exportExternalActions = exportActions.where(EntryActions.exportExternal.contains).toList();
|
||||
return [
|
||||
if (pageEntry.canRotateAndFlip) _buildRotateAndFlipMenuItems(context),
|
||||
...topLevelActions.map((action) => _buildPopupMenuItem(context, action)),
|
||||
if (exportActions.isNotEmpty)
|
||||
PopupMenuItem<EntryAction>(
|
||||
padding: EdgeInsets.zero,
|
||||
child: PopupMenuItemExpansionPanel<EntryAction>(
|
||||
icon: AIcons.export,
|
||||
title: context.l10n.entryActionExport,
|
||||
items: [
|
||||
...exportInternalActions.map((action) => _buildPopupMenuItem(context, action)).toList(),
|
||||
if (exportInternalActions.isNotEmpty && exportExternalActions.isNotEmpty) const PopupMenuDivider(height: 0),
|
||||
...exportExternalActions.map((action) => _buildPopupMenuItem(context, action)).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!kReleaseMode) ...[
|
||||
const PopupMenuDivider(),
|
||||
_buildPopupMenuItem(context, EntryAction.debug),
|
||||
]
|
||||
];
|
||||
},
|
||||
onSelected: (action) {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action));
|
||||
},
|
||||
onMenuOpened: () {
|
||||
// if the menu is opened while overlay is hiding,
|
||||
// the popup menu button is disposed and menu items are ineffective,
|
||||
// so we make sure overlay stays visible
|
||||
const ToggleOverlayNotification(visible: true).dispatch(context);
|
||||
},
|
||||
if (showInfo)
|
||||
BlurredRect(
|
||||
enabled: blurred,
|
||||
child: Container(
|
||||
color: overlayBackgroundColor(blurred: blurred),
|
||||
child: SafeArea(
|
||||
minimum: EdgeInsets.only(top: (viewInsets?.top ?? 0) + (viewPadding?.top ?? 0)),
|
||||
bottom: false,
|
||||
child: ViewerDetailOverlay(
|
||||
index: index,
|
||||
entries: entries,
|
||||
hasCollection: hasCollection,
|
||||
multiPageController: multiPageController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (settings.showOverlayMinimap)
|
||||
SafeArea(
|
||||
top: !showInfo,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: FadeTransition(
|
||||
opacity: scale,
|
||||
child: Minimap(
|
||||
viewStateNotifier: viewStateNotifier,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverlayButton(BuildContext context, EntryAction action) {
|
||||
Widget? child;
|
||||
void onPressed() => _onActionSelected(context, action);
|
||||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
child = FavouriteToggler(
|
||||
entries: {favouriteTargetEntry},
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
child = IconButton(
|
||||
icon: action.getIcon(),
|
||||
onPressed: onPressed,
|
||||
tooltip: action.getText(context),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: ViewerTopOverlay.innerPadding),
|
||||
child: OverlayButton(
|
||||
scale: scale,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<EntryAction> _buildPopupMenuItem(BuildContext context, EntryAction action) {
|
||||
Widget? child;
|
||||
switch (action) {
|
||||
// in app actions
|
||||
case EntryAction.toggleFavourite:
|
||||
child = FavouriteToggler(
|
||||
entries: {favouriteTargetEntry},
|
||||
isMenuItem: true,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
||||
break;
|
||||
}
|
||||
return PopupMenuItem(
|
||||
value: action,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<EntryAction> _buildRotateAndFlipMenuItems(BuildContext context) {
|
||||
Widget buildDivider() => const SizedBox(
|
||||
height: 16,
|
||||
child: VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
);
|
||||
|
||||
Widget buildItem(EntryAction action) => Expanded(
|
||||
child: PopupMenuItem(
|
||||
value: action,
|
||||
child: Tooltip(
|
||||
message: action.getText(context),
|
||||
child: Center(child: action.getIcon()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
buildDivider(),
|
||||
buildItem(EntryAction.rotateCCW),
|
||||
buildDivider(),
|
||||
buildItem(EntryAction.rotateCW),
|
||||
buildDivider(),
|
||||
buildItem(EntryAction.flip),
|
||||
buildDivider(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onActionSelected(BuildContext context, EntryAction action) {
|
||||
var targetEntry = mainEntry;
|
||||
if (mainEntry.isMultiPage && (mainEntry.isBurst || EntryActions.pageActions.contains(action))) {
|
||||
final multiPageController = context.read<MultiPageConductor>().getController(mainEntry);
|
||||
if (multiPageController != null) {
|
||||
final multiPageInfo = multiPageController.info;
|
||||
final pageEntry = multiPageInfo?.getPageEntryByIndex(multiPageController.page);
|
||||
if (pageEntry != null) {
|
||||
targetEntry = pageEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
EntryActionDelegate(targetEntry).onActionSelected(context, action);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
|
@ -15,7 +15,7 @@ import 'package:provider/provider.dart';
|
|||
class VideoControlRow extends StatelessWidget {
|
||||
final AvesVideoController? controller;
|
||||
final Animation<double> scale;
|
||||
final Function(VideoAction value) onActionSelected;
|
||||
final Function(EntryAction value) onActionSelected;
|
||||
|
||||
static const double padding = 8;
|
||||
static const Radius radius = Radius.circular(123);
|
||||
|
@ -41,7 +41,7 @@ class VideoControlRow extends StatelessWidget {
|
|||
child: _buildOverlayButton(
|
||||
child: PlayToggler(
|
||||
controller: controller,
|
||||
onPressed: () => onActionSelected(VideoAction.togglePlay),
|
||||
onPressed: () => onActionSelected(EntryAction.videoTogglePlay),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -52,30 +52,28 @@ class VideoControlRow extends StatelessWidget {
|
|||
const SizedBox(width: padding),
|
||||
_buildIconButton(
|
||||
context,
|
||||
VideoAction.replay10,
|
||||
EntryAction.videoReplay10,
|
||||
borderRadius: const BorderRadius.only(topLeft: radius, bottomLeft: radius),
|
||||
),
|
||||
_buildOverlayButton(
|
||||
child: PlayToggler(
|
||||
controller: controller,
|
||||
onPressed: () => onActionSelected(VideoAction.togglePlay),
|
||||
onPressed: () => onActionSelected(EntryAction.videoTogglePlay),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.zero),
|
||||
),
|
||||
_buildIconButton(
|
||||
context,
|
||||
VideoAction.skip10,
|
||||
EntryAction.videoSkip10,
|
||||
borderRadius: const BorderRadius.only(topRight: radius, bottomRight: radius),
|
||||
),
|
||||
],
|
||||
);
|
||||
case VideoControls.playOutside:
|
||||
final trashed = controller?.entry.trashed ?? false;
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: padding),
|
||||
child: _buildIconButton(
|
||||
context,
|
||||
VideoAction.playOutside,
|
||||
),
|
||||
child: _buildIconButton(context, EntryAction.open, enabled: !trashed),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -94,14 +92,15 @@ class VideoControlRow extends StatelessWidget {
|
|||
|
||||
Widget _buildIconButton(
|
||||
BuildContext context,
|
||||
VideoAction action, {
|
||||
EntryAction action, {
|
||||
bool enabled = true,
|
||||
BorderRadius? borderRadius,
|
||||
}) =>
|
||||
_buildOverlayButton(
|
||||
borderRadius: borderRadius,
|
||||
child: IconButton(
|
||||
icon: action.getIcon(),
|
||||
onPressed: () => onActionSelected(action),
|
||||
onPressed: enabled ? () => onActionSelected(action) : null,
|
||||
tooltip: action.getText(context),
|
||||
),
|
||||
);
|
81
lib/widgets/viewer/overlay/video/video.dart
Normal file
81
lib/widgets/viewer/overlay/video/video.dart
Normal file
|
@ -0,0 +1,81 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/controls.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoControlOverlay extends StatefulWidget {
|
||||
final AvesEntry entry;
|
||||
final AvesVideoController? controller;
|
||||
final Animation<double> scale;
|
||||
final Function(EntryAction value) onActionSelected;
|
||||
final VoidCallback onActionMenuOpened;
|
||||
|
||||
const VideoControlOverlay({
|
||||
Key? key,
|
||||
required this.entry,
|
||||
required this.controller,
|
||||
required this.scale,
|
||||
required this.onActionSelected,
|
||||
required this.onActionMenuOpened,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VideoControlOverlayState();
|
||||
}
|
||||
|
||||
class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTickerProviderStateMixin {
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
||||
Animation<double> get scale => widget.scale;
|
||||
|
||||
AvesVideoController? get controller => widget.controller;
|
||||
|
||||
Stream<VideoStatus> get statusStream => controller?.statusStream ?? Stream.value(VideoStatus.idle);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<VideoStatus>(
|
||||
stream: statusStream,
|
||||
builder: (context, snapshot) {
|
||||
// do not use stream snapshot because it is obsolete when switching between videos
|
||||
final status = controller?.status ?? VideoStatus.idle;
|
||||
|
||||
if (status == VideoStatus.error) {
|
||||
const action = EntryAction.open;
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: OverlayButton(
|
||||
scale: scale,
|
||||
child: IconButton(
|
||||
icon: action.getIcon(),
|
||||
onPressed: entry.trashed ? null : () => widget.onActionSelected(action),
|
||||
tooltip: action.getText(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: VideoProgressBar(
|
||||
controller: controller,
|
||||
scale: scale,
|
||||
),
|
||||
),
|
||||
VideoControlRow(
|
||||
controller: controller,
|
||||
scale: scale,
|
||||
onActionSelected: widget.onActionSelected,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
377
lib/widgets/viewer/overlay/viewer_button_row.dart
Normal file
377
lib/widgets/viewer/overlay/viewer_button_row.dart
Normal file
|
@ -0,0 +1,377 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/favourite_toggler.dart';
|
||||
import 'package:aves/widgets/viewer/action/entry_action_delegate.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/controls.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ViewerButtonRow extends StatelessWidget {
|
||||
final AvesEntry mainEntry;
|
||||
final AvesEntry pageEntry;
|
||||
final Animation<double> scale;
|
||||
final bool canToggleFavourite;
|
||||
|
||||
static const double outerPadding = 8;
|
||||
static const double innerPadding = 8;
|
||||
|
||||
const ViewerButtonRow({
|
||||
Key? key,
|
||||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
required this.scale,
|
||||
required this.canToggleFavourite,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final trashed = mainEntry.trashed;
|
||||
|
||||
bool _isVisible(EntryAction action) {
|
||||
if (trashed) {
|
||||
switch (action) {
|
||||
case EntryAction.delete:
|
||||
case EntryAction.restore:
|
||||
return true;
|
||||
case EntryAction.debug:
|
||||
return kDebugMode;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry;
|
||||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
return canToggleFavourite;
|
||||
case EntryAction.delete:
|
||||
case EntryAction.rename:
|
||||
case EntryAction.copy:
|
||||
case EntryAction.move:
|
||||
return targetEntry.canEdit;
|
||||
case EntryAction.rotateCCW:
|
||||
case EntryAction.rotateCW:
|
||||
case EntryAction.flip:
|
||||
return targetEntry.canRotateAndFlip;
|
||||
case EntryAction.convert:
|
||||
case EntryAction.print:
|
||||
return !targetEntry.isVideo && device.canPrint;
|
||||
case EntryAction.openMap:
|
||||
return targetEntry.hasGps;
|
||||
case EntryAction.viewSource:
|
||||
return targetEntry.isSvg;
|
||||
case EntryAction.videoCaptureFrame:
|
||||
case EntryAction.videoSelectStreams:
|
||||
case EntryAction.videoSetSpeed:
|
||||
case EntryAction.videoSettings:
|
||||
case EntryAction.videoTogglePlay:
|
||||
case EntryAction.videoReplay10:
|
||||
case EntryAction.videoSkip10:
|
||||
return targetEntry.isVideo;
|
||||
case EntryAction.rotateScreen:
|
||||
return settings.isRotationLocked;
|
||||
case EntryAction.addShortcut:
|
||||
return device.canPinShortcut;
|
||||
case EntryAction.copyToClipboard:
|
||||
case EntryAction.edit:
|
||||
case EntryAction.open:
|
||||
case EntryAction.setAs:
|
||||
case EntryAction.share:
|
||||
return true;
|
||||
case EntryAction.restore:
|
||||
return false;
|
||||
case EntryAction.debug:
|
||||
return kDebugMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.size.width - mq.padding.horizontal,
|
||||
builder: (context, mqWidth, child) {
|
||||
final buttonWidth = OverlayButton.getSize(context);
|
||||
final availableCount = ((mqWidth - outerPadding * 2) / (buttonWidth + innerPadding)).floor();
|
||||
return Selector<Settings, bool>(
|
||||
selector: (context, s) => s.isRotationLocked,
|
||||
builder: (context, s, child) {
|
||||
final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(_isVisible).take(availableCount - 1).toList();
|
||||
final topLevelActions = EntryActions.topLevel.where((action) => !quickActions.contains(action)).where(_isVisible).toList();
|
||||
final exportActions = EntryActions.export.where((action) => !quickActions.contains(action)).where(_isVisible).toList();
|
||||
final videoActions = EntryActions.video.where((action) => !quickActions.contains(action)).where(_isVisible).toList();
|
||||
return ViewerButtonRowContent(
|
||||
quickActions: quickActions,
|
||||
topLevelActions: topLevelActions,
|
||||
exportActions: exportActions,
|
||||
videoActions: videoActions,
|
||||
scale: scale,
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewerButtonRowContent extends StatelessWidget {
|
||||
final List<EntryAction> quickActions, topLevelActions, exportActions, videoActions;
|
||||
final Animation<double> scale;
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
|
||||
AvesEntry get favouriteTargetEntry => mainEntry.isBurst ? pageEntry : mainEntry;
|
||||
|
||||
static const double padding = 8;
|
||||
|
||||
const ViewerButtonRowContent({
|
||||
Key? key,
|
||||
required this.quickActions,
|
||||
required this.topLevelActions,
|
||||
required this.exportActions,
|
||||
required this.videoActions,
|
||||
required this.scale,
|
||||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasOverflowMenu = pageEntry.canRotateAndFlip || topLevelActions.isNotEmpty || exportActions.isNotEmpty || videoActions.isNotEmpty;
|
||||
return Selector<VideoConductor, AvesVideoController?>(
|
||||
selector: (context, vc) => vc.getController(pageEntry),
|
||||
builder: (context, videoController, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: padding / 2, right: padding / 2, bottom: padding),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
...quickActions.map((action) => _buildOverlayButton(context, action, videoController)),
|
||||
if (hasOverflowMenu)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: padding / 2),
|
||||
child: OverlayButton(
|
||||
scale: scale,
|
||||
child: MenuIconTheme(
|
||||
child: AvesPopupMenuButton<EntryAction>(
|
||||
key: const Key('entry-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
final exportInternalActions = exportActions.whereNot(EntryActions.exportExternal.contains).toList();
|
||||
final exportExternalActions = exportActions.where(EntryActions.exportExternal.contains).toList();
|
||||
return [
|
||||
if (pageEntry.canRotateAndFlip) _buildRotateAndFlipMenuItems(context),
|
||||
...topLevelActions.map((action) => _buildPopupMenuItem(context, action, videoController)),
|
||||
if (exportActions.isNotEmpty)
|
||||
PopupMenuItem<EntryAction>(
|
||||
padding: EdgeInsets.zero,
|
||||
child: PopupMenuItemExpansionPanel<EntryAction>(
|
||||
icon: AIcons.export,
|
||||
title: context.l10n.entryActionExport,
|
||||
items: [
|
||||
...exportInternalActions.map((action) => _buildPopupMenuItem(context, action, videoController)).toList(),
|
||||
if (exportInternalActions.isNotEmpty && exportExternalActions.isNotEmpty) const PopupMenuDivider(height: 0),
|
||||
...exportExternalActions.map((action) => _buildPopupMenuItem(context, action, videoController)).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (videoActions.isNotEmpty)
|
||||
PopupMenuItem<EntryAction>(
|
||||
padding: EdgeInsets.zero,
|
||||
child: PopupMenuItemExpansionPanel<EntryAction>(
|
||||
icon: AIcons.video,
|
||||
title: context.l10n.settingsSectionVideo,
|
||||
items: [
|
||||
...videoActions.map((action) => _buildPopupMenuItem(context, action, videoController)).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!kReleaseMode) ...[
|
||||
const PopupMenuDivider(),
|
||||
_buildPopupMenuItem(context, EntryAction.debug, videoController),
|
||||
]
|
||||
];
|
||||
},
|
||||
onSelected: (action) {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action));
|
||||
},
|
||||
onMenuOpened: () {
|
||||
// if the menu is opened while overlay is hiding,
|
||||
// the popup menu button is disposed and menu items are ineffective,
|
||||
// so we make sure overlay stays visible
|
||||
const ToggleOverlayNotification(visible: true).dispatch(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverlayButton(BuildContext context, EntryAction action, AvesVideoController? videoController) {
|
||||
Widget? child;
|
||||
void onPressed() => _onActionSelected(context, action);
|
||||
|
||||
ValueListenableBuilder<bool> _buildFromListenable(ValueListenable<bool>? enabledNotifier) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: enabledNotifier ?? ValueNotifier(false),
|
||||
builder: (context, canDo, child) => IconButton(
|
||||
icon: child!,
|
||||
onPressed: canDo ? onPressed : null,
|
||||
tooltip: action.getText(context),
|
||||
),
|
||||
child: action.getIcon(),
|
||||
);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
child = FavouriteToggler(
|
||||
entries: {favouriteTargetEntry},
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
case EntryAction.videoTogglePlay:
|
||||
child = PlayToggler(
|
||||
controller: videoController,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
case EntryAction.videoCaptureFrame:
|
||||
child = _buildFromListenable(videoController?.canCaptureFrameNotifier);
|
||||
break;
|
||||
case EntryAction.videoSelectStreams:
|
||||
child = _buildFromListenable(videoController?.canSelectStreamNotifier);
|
||||
break;
|
||||
case EntryAction.videoSetSpeed:
|
||||
child = _buildFromListenable(videoController?.canSetSpeedNotifier);
|
||||
break;
|
||||
default:
|
||||
child = IconButton(
|
||||
icon: action.getIcon(),
|
||||
onPressed: onPressed,
|
||||
tooltip: action.getText(context),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: padding / 2),
|
||||
child: OverlayButton(
|
||||
scale: scale,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<EntryAction> _buildPopupMenuItem(BuildContext context, EntryAction action, AvesVideoController? videoController) {
|
||||
late final bool enabled;
|
||||
switch (action) {
|
||||
case EntryAction.videoCaptureFrame:
|
||||
enabled = videoController?.canCaptureFrameNotifier.value ?? false;
|
||||
break;
|
||||
case EntryAction.videoSelectStreams:
|
||||
enabled = videoController?.canSelectStreamNotifier.value ?? false;
|
||||
break;
|
||||
case EntryAction.videoSetSpeed:
|
||||
enabled = videoController?.canSetSpeedNotifier.value ?? false;
|
||||
break;
|
||||
default:
|
||||
enabled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Widget? child;
|
||||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
child = FavouriteToggler(
|
||||
entries: {favouriteTargetEntry},
|
||||
isMenuItem: true,
|
||||
);
|
||||
break;
|
||||
case EntryAction.videoTogglePlay:
|
||||
child = PlayToggler(
|
||||
controller: videoController,
|
||||
isMenuItem: true,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
||||
break;
|
||||
}
|
||||
return PopupMenuItem(
|
||||
value: action,
|
||||
enabled: enabled,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<EntryAction> _buildRotateAndFlipMenuItems(BuildContext context) {
|
||||
Widget buildDivider() => const SizedBox(
|
||||
height: 16,
|
||||
child: VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
);
|
||||
|
||||
Widget buildItem(EntryAction action) => Expanded(
|
||||
child: PopupMenuItem(
|
||||
value: action,
|
||||
child: Tooltip(
|
||||
message: action.getText(context),
|
||||
child: Center(child: action.getIcon()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
buildDivider(),
|
||||
buildItem(EntryAction.rotateCCW),
|
||||
buildDivider(),
|
||||
buildItem(EntryAction.rotateCW),
|
||||
buildDivider(),
|
||||
buildItem(EntryAction.flip),
|
||||
buildDivider(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onActionSelected(BuildContext context, EntryAction action) {
|
||||
var targetEntry = mainEntry;
|
||||
if (mainEntry.isMultiPage && (mainEntry.isBurst || EntryActions.pageActions.contains(action))) {
|
||||
final multiPageController = context.read<MultiPageConductor>().getController(mainEntry);
|
||||
if (multiPageController != null) {
|
||||
final multiPageInfo = multiPageController.info;
|
||||
final pageEntry = multiPageInfo?.getPageEntryByIndex(multiPageController.page);
|
||||
if (pageEntry != null) {
|
||||
targetEntry = pageEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
EntryActionDelegate(targetEntry).onActionSelected(context, action);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -13,6 +13,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
|||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/video_speed_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart';
|
||||
import 'package:aves/widgets/settings/video/video.dart';
|
||||
|
@ -34,37 +35,41 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
stopOverlayHidingTimer();
|
||||
}
|
||||
|
||||
void onActionSelected(BuildContext context, AvesVideoController controller, VideoAction action) {
|
||||
void onActionSelected(BuildContext context, AvesVideoController controller, EntryAction action) {
|
||||
// make sure overlay is not disappearing when selecting an action
|
||||
stopOverlayHidingTimer();
|
||||
const ToggleOverlayNotification(visible: true).dispatch(context);
|
||||
|
||||
switch (action) {
|
||||
case VideoAction.captureFrame:
|
||||
case EntryAction.videoCaptureFrame:
|
||||
_captureFrame(context, controller);
|
||||
break;
|
||||
case VideoAction.playOutside:
|
||||
final entry = controller.entry;
|
||||
androidAppService.open(entry.uri, entry.mimeTypeAnySubtype);
|
||||
break;
|
||||
case VideoAction.replay10:
|
||||
if (controller.isReady) controller.seekTo(controller.currentPosition - 10000);
|
||||
break;
|
||||
case VideoAction.skip10:
|
||||
if (controller.isReady) controller.seekTo(controller.currentPosition + 10000);
|
||||
break;
|
||||
case VideoAction.selectStreams:
|
||||
case EntryAction.videoSelectStreams:
|
||||
_showStreamSelectionDialog(context, controller);
|
||||
break;
|
||||
case VideoAction.setSpeed:
|
||||
case EntryAction.videoSetSpeed:
|
||||
_showSpeedDialog(context, controller);
|
||||
break;
|
||||
case VideoAction.settings:
|
||||
case EntryAction.videoSettings:
|
||||
_showSettings(context, controller);
|
||||
break;
|
||||
case VideoAction.togglePlay:
|
||||
case EntryAction.videoTogglePlay:
|
||||
_togglePlayPause(context, controller);
|
||||
break;
|
||||
case EntryAction.videoReplay10:
|
||||
if (controller.isReady) controller.seekTo(controller.currentPosition - 10000);
|
||||
break;
|
||||
case EntryAction.videoSkip10:
|
||||
if (controller.isReady) controller.seekTo(controller.currentPosition + 10000);
|
||||
break;
|
||||
case EntryAction.open:
|
||||
final entry = controller.entry;
|
||||
androidAppService.open(entry.uri, entry.mimeTypeAnySubtype).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw UnsupportedError('$action is not a video action');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
|
@ -194,7 +194,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
if (videoController == null) return const SizedBox();
|
||||
|
||||
Positioned _buildDoubleTapDetector(
|
||||
VideoAction action, {
|
||||
EntryAction action, {
|
||||
double widthFactor = 1,
|
||||
AlignmentGeometry alignment = Alignment.center,
|
||||
IconData? Function()? icon,
|
||||
|
@ -215,7 +215,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
)
|
||||
],
|
||||
);
|
||||
VideoGestureNotification(
|
||||
VideoActionNotification(
|
||||
controller: videoController,
|
||||
action: action,
|
||||
).dispatch(context);
|
||||
|
@ -256,12 +256,12 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
),
|
||||
if (playGesture)
|
||||
_buildDoubleTapDetector(
|
||||
VideoAction.togglePlay,
|
||||
EntryAction.videoTogglePlay,
|
||||
icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
|
||||
),
|
||||
if (seekGesture) ...[
|
||||
_buildDoubleTapDetector(VideoAction.replay10, widthFactor: .25, alignment: Alignment.centerLeft),
|
||||
_buildDoubleTapDetector(VideoAction.skip10, widthFactor: .25, alignment: Alignment.centerRight),
|
||||
_buildDoubleTapDetector(EntryAction.videoReplay10, widthFactor: .25, alignment: Alignment.centerLeft),
|
||||
_buildDoubleTapDetector(EntryAction.videoSkip10, widthFactor: .25, alignment: Alignment.centerRight),
|
||||
],
|
||||
if (useActionGesture)
|
||||
ValueListenableBuilder<Widget?>(
|
||||
|
|
Loading…
Reference in a new issue