#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",
|
"settingsVideoEnableAutoPlay": "Automatische Wiedergabe",
|
||||||
"settingsVideoLoopModeTile": "Schleifen-Modus",
|
"settingsVideoLoopModeTile": "Schleifen-Modus",
|
||||||
"settingsVideoLoopModeTitle": "Schleifen-Modus",
|
"settingsVideoLoopModeTitle": "Schleifen-Modus",
|
||||||
"settingsVideoQuickActionsTile": "Schnelle Aktionen für Videos",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "Schnelle Aktionen",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Untertitel",
|
"settingsSubtitleThemeTile": "Untertitel",
|
||||||
"settingsSubtitleThemeTitle": "Untertitel",
|
"settingsSubtitleThemeTitle": "Untertitel",
|
||||||
|
|
|
@ -155,7 +155,7 @@
|
||||||
"videoControlsNone": "None",
|
"videoControlsNone": "None",
|
||||||
"videoControlsPlay": "Play",
|
"videoControlsPlay": "Play",
|
||||||
"videoControlsPlaySeek": "Play & seek",
|
"videoControlsPlaySeek": "Play & seek",
|
||||||
"videoControlsPlayOutside": "Play outside",
|
"videoControlsPlayOutside": "Open with other player",
|
||||||
|
|
||||||
"mapStyleGoogleNormal": "Google Maps",
|
"mapStyleGoogleNormal": "Google Maps",
|
||||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||||
|
@ -628,8 +628,6 @@
|
||||||
"settingsVideoEnableAutoPlay": "Auto play",
|
"settingsVideoEnableAutoPlay": "Auto play",
|
||||||
"settingsVideoLoopModeTile": "Loop mode",
|
"settingsVideoLoopModeTile": "Loop mode",
|
||||||
"settingsVideoLoopModeTitle": "Loop Mode",
|
"settingsVideoLoopModeTitle": "Loop Mode",
|
||||||
"settingsVideoQuickActionsTile": "Quick actions for videos",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "Quick Actions",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Subtitles",
|
"settingsSubtitleThemeTile": "Subtitles",
|
||||||
"settingsSubtitleThemeTitle": "Subtitles",
|
"settingsSubtitleThemeTitle": "Subtitles",
|
||||||
|
|
|
@ -442,8 +442,6 @@
|
||||||
"settingsVideoEnableAutoPlay": "Reproducción automática",
|
"settingsVideoEnableAutoPlay": "Reproducción automática",
|
||||||
"settingsVideoLoopModeTile": "Modo bucle",
|
"settingsVideoLoopModeTile": "Modo bucle",
|
||||||
"settingsVideoLoopModeTitle": "Modo bucle",
|
"settingsVideoLoopModeTitle": "Modo bucle",
|
||||||
"settingsVideoQuickActionsTile": "Acciones rápidas para videos",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "Acciones rápidas",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Subtítulos",
|
"settingsSubtitleThemeTile": "Subtítulos",
|
||||||
"settingsSubtitleThemeTitle": "Subtítulos",
|
"settingsSubtitleThemeTitle": "Subtítulos",
|
||||||
|
|
|
@ -440,8 +440,6 @@
|
||||||
"settingsVideoEnableAutoPlay": "Lecture automatique",
|
"settingsVideoEnableAutoPlay": "Lecture automatique",
|
||||||
"settingsVideoLoopModeTile": "Lecture répétée",
|
"settingsVideoLoopModeTile": "Lecture répétée",
|
||||||
"settingsVideoLoopModeTitle": "Lecture répétée",
|
"settingsVideoLoopModeTitle": "Lecture répétée",
|
||||||
"settingsVideoQuickActionsTile": "Actions rapides pour les vidéos",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "Actions rapides",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Sous-titres",
|
"settingsSubtitleThemeTile": "Sous-titres",
|
||||||
"settingsSubtitleThemeTitle": "Sous-titres",
|
"settingsSubtitleThemeTitle": "Sous-titres",
|
||||||
|
|
|
@ -439,8 +439,6 @@
|
||||||
"settingsVideoEnableAutoPlay": "Putar otomatis",
|
"settingsVideoEnableAutoPlay": "Putar otomatis",
|
||||||
"settingsVideoLoopModeTile": "Putar ulang",
|
"settingsVideoLoopModeTile": "Putar ulang",
|
||||||
"settingsVideoLoopModeTitle": "Putar Ulang",
|
"settingsVideoLoopModeTitle": "Putar Ulang",
|
||||||
"settingsVideoQuickActionsTile": "Aksi cepat untuk video",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "Aksi Cepat",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Subtitle",
|
"settingsSubtitleThemeTile": "Subtitle",
|
||||||
"settingsSubtitleThemeTitle": "Subtitle",
|
"settingsSubtitleThemeTitle": "Subtitle",
|
||||||
|
|
|
@ -440,8 +440,6 @@
|
||||||
"settingsVideoEnableAutoPlay": "자동 재생",
|
"settingsVideoEnableAutoPlay": "자동 재생",
|
||||||
"settingsVideoLoopModeTile": "반복 모드",
|
"settingsVideoLoopModeTile": "반복 모드",
|
||||||
"settingsVideoLoopModeTitle": "반복 모드",
|
"settingsVideoLoopModeTitle": "반복 모드",
|
||||||
"settingsVideoQuickActionsTile": "동영상의 빠른 작업",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "빠른 작업",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "자막",
|
"settingsSubtitleThemeTile": "자막",
|
||||||
"settingsSubtitleThemeTitle": "자막",
|
"settingsSubtitleThemeTitle": "자막",
|
||||||
|
|
|
@ -440,8 +440,6 @@
|
||||||
"settingsVideoEnableAutoPlay": "Reprodução automática",
|
"settingsVideoEnableAutoPlay": "Reprodução automática",
|
||||||
"settingsVideoLoopModeTile": "Modo de loop",
|
"settingsVideoLoopModeTile": "Modo de loop",
|
||||||
"settingsVideoLoopModeTitle": "Modo de loop",
|
"settingsVideoLoopModeTitle": "Modo de loop",
|
||||||
"settingsVideoQuickActionsTile": "Ações rápidas para vídeos",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "Ações rápidas",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Legendas",
|
"settingsSubtitleThemeTile": "Legendas",
|
||||||
"settingsSubtitleThemeTitle": "Legendas",
|
"settingsSubtitleThemeTitle": "Legendas",
|
||||||
|
|
|
@ -439,8 +439,6 @@
|
||||||
"settingsVideoEnableAutoPlay": "Автозапуск воспроизведения",
|
"settingsVideoEnableAutoPlay": "Автозапуск воспроизведения",
|
||||||
"settingsVideoLoopModeTile": "Циклический режим",
|
"settingsVideoLoopModeTile": "Циклический режим",
|
||||||
"settingsVideoLoopModeTitle": "Цикличный режим",
|
"settingsVideoLoopModeTitle": "Цикличный режим",
|
||||||
"settingsVideoQuickActionsTile": "Быстрые действия для видео",
|
|
||||||
"settingsVideoQuickActionEditorTitle": "Быстрые действия",
|
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Субтитры",
|
"settingsSubtitleThemeTile": "Субтитры",
|
||||||
"settingsSubtitleThemeTitle": "Субтитры",
|
"settingsSubtitleThemeTitle": "Субтитры",
|
||||||
|
|
|
@ -21,6 +21,14 @@ enum EntryAction {
|
||||||
flip,
|
flip,
|
||||||
// vector
|
// vector
|
||||||
viewSource,
|
viewSource,
|
||||||
|
// video
|
||||||
|
videoCaptureFrame,
|
||||||
|
videoSelectStreams,
|
||||||
|
videoSetSpeed,
|
||||||
|
videoSettings,
|
||||||
|
videoTogglePlay,
|
||||||
|
videoReplay10,
|
||||||
|
videoSkip10,
|
||||||
// external
|
// external
|
||||||
edit,
|
edit,
|
||||||
open,
|
open,
|
||||||
|
@ -41,8 +49,8 @@ class EntryActions {
|
||||||
EntryAction.copy,
|
EntryAction.copy,
|
||||||
EntryAction.move,
|
EntryAction.move,
|
||||||
EntryAction.toggleFavourite,
|
EntryAction.toggleFavourite,
|
||||||
EntryAction.viewSource,
|
|
||||||
EntryAction.rotateScreen,
|
EntryAction.rotateScreen,
|
||||||
|
EntryAction.viewSource,
|
||||||
];
|
];
|
||||||
|
|
||||||
static const export = [
|
static const export = [
|
||||||
|
@ -62,6 +70,13 @@ class EntryActions {
|
||||||
];
|
];
|
||||||
|
|
||||||
static const pageActions = [
|
static const pageActions = [
|
||||||
|
EntryAction.videoCaptureFrame,
|
||||||
|
EntryAction.videoSelectStreams,
|
||||||
|
EntryAction.videoSetSpeed,
|
||||||
|
EntryAction.videoSettings,
|
||||||
|
EntryAction.videoTogglePlay,
|
||||||
|
EntryAction.videoReplay10,
|
||||||
|
EntryAction.videoSkip10,
|
||||||
EntryAction.rotateCCW,
|
EntryAction.rotateCCW,
|
||||||
EntryAction.rotateCW,
|
EntryAction.rotateCW,
|
||||||
EntryAction.flip,
|
EntryAction.flip,
|
||||||
|
@ -72,6 +87,13 @@ class EntryActions {
|
||||||
EntryAction.restore,
|
EntryAction.restore,
|
||||||
EntryAction.debug,
|
EntryAction.debug,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static const video = [
|
||||||
|
EntryAction.videoCaptureFrame,
|
||||||
|
EntryAction.videoSetSpeed,
|
||||||
|
EntryAction.videoSelectStreams,
|
||||||
|
EntryAction.videoSettings,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExtraEntryAction on EntryAction {
|
extension ExtraEntryAction on EntryAction {
|
||||||
|
@ -110,6 +132,22 @@ extension ExtraEntryAction on EntryAction {
|
||||||
// vector
|
// vector
|
||||||
case EntryAction.viewSource:
|
case EntryAction.viewSource:
|
||||||
return context.l10n.entryActionViewSource;
|
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
|
// external
|
||||||
case EntryAction.edit:
|
case EntryAction.edit:
|
||||||
return context.l10n.entryActionEdit;
|
return context.l10n.entryActionEdit;
|
||||||
|
@ -176,6 +214,22 @@ extension ExtraEntryAction on EntryAction {
|
||||||
// vector
|
// vector
|
||||||
case EntryAction.viewSource:
|
case EntryAction.viewSource:
|
||||||
return AIcons.vector;
|
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
|
// external
|
||||||
case EntryAction.edit:
|
case EntryAction.edit:
|
||||||
return AIcons.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_actions.dart';
|
||||||
import 'package:aves/model/actions/entry_set_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/favourite.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
|
@ -74,10 +73,6 @@ class SettingsDefaults {
|
||||||
static const enableMotionPhotoAutoPlay = false;
|
static const enableMotionPhotoAutoPlay = false;
|
||||||
|
|
||||||
// video
|
// video
|
||||||
static const videoQuickActions = [
|
|
||||||
VideoAction.replay10,
|
|
||||||
VideoAction.togglePlay,
|
|
||||||
];
|
|
||||||
static const enableVideoHardwareAcceleration = true;
|
static const enableVideoHardwareAcceleration = true;
|
||||||
static const enableVideoAutoPlay = false;
|
static const enableVideoAutoPlay = false;
|
||||||
static const videoLoopMode = VideoLoopMode.shortOnly;
|
static const videoLoopMode = VideoLoopMode.shortOnly;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:math';
|
||||||
import 'package:aves/l10n/l10n.dart';
|
import 'package:aves/l10n/l10n.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/actions/entry_set_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/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/defaults.dart';
|
import 'package:aves/model/settings/defaults.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
|
@ -90,7 +89,6 @@ class Settings extends ChangeNotifier {
|
||||||
static const imageBackgroundKey = 'image_background';
|
static const imageBackgroundKey = 'image_background';
|
||||||
|
|
||||||
// video
|
// video
|
||||||
static const videoQuickActionsKey = 'video_quick_actions';
|
|
||||||
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
||||||
static const enableVideoAutoPlayKey = 'video_auto_play';
|
static const enableVideoAutoPlayKey = 'video_auto_play';
|
||||||
static const videoLoopModeKey = 'video_loop';
|
static const videoLoopModeKey = 'video_loop';
|
||||||
|
@ -419,10 +417,6 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
// video
|
// 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);
|
bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, SettingsDefaults.enableVideoHardwareAcceleration);
|
||||||
|
|
||||||
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
|
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
|
||||||
|
@ -713,7 +707,6 @@ class Settings extends ChangeNotifier {
|
||||||
case collectionBrowsingQuickActionsKey:
|
case collectionBrowsingQuickActionsKey:
|
||||||
case collectionSelectionQuickActionsKey:
|
case collectionSelectionQuickActionsKey:
|
||||||
case viewerQuickActionsKey:
|
case viewerQuickActionsKey:
|
||||||
case videoQuickActionsKey:
|
|
||||||
if (newValue is List) {
|
if (newValue is List) {
|
||||||
settingsStore.setStringList(key, newValue.cast<String>());
|
settingsStore.setStringList(key, newValue.cast<String>());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -59,7 +59,6 @@ class DebugSettingsSection extends StatelessWidget {
|
||||||
'infoMapZoom': '${settings.infoMapZoom}',
|
'infoMapZoom': '${settings.infoMapZoom}',
|
||||||
'collectionSelectionQuickActions': '${settings.collectionSelectionQuickActions}',
|
'collectionSelectionQuickActions': '${settings.collectionSelectionQuickActions}',
|
||||||
'viewerQuickActions': '${settings.viewerQuickActions}',
|
'viewerQuickActions': '${settings.viewerQuickActions}',
|
||||||
'videoQuickActions': '${settings.videoQuickActions}',
|
|
||||||
'drawerTypeBookmarks': toMultiline(settings.drawerTypeBookmarks),
|
'drawerTypeBookmarks': toMultiline(settings.drawerTypeBookmarks),
|
||||||
'drawerAlbumBookmarks': toMultiline(settings.drawerAlbumBookmarks),
|
'drawerAlbumBookmarks': toMultiline(settings.drawerAlbumBookmarks),
|
||||||
'drawerPageBookmarks': toMultiline(settings.drawerPageBookmarks),
|
'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/common/tile_leading.dart';
|
||||||
import 'package:aves/widgets/settings/video/controls.dart';
|
import 'package:aves/widgets/settings/video/controls.dart';
|
||||||
import 'package:aves/widgets/settings/video/subtitle_theme.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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -37,7 +36,6 @@ class VideoSection extends StatelessWidget {
|
||||||
title: Text(context.l10n.settingsVideoShowVideos),
|
title: Text(context.l10n.settingsVideoShowVideos),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VideoActionsTile(),
|
|
||||||
Selector<Settings, bool>(
|
Selector<Settings, bool>(
|
||||||
selector: (context, s) => s.enableVideoHardwareAcceleration,
|
selector: (context, s) => s.enableVideoHardwareAcceleration,
|
||||||
builder: (context, current, child) => SwitchListTile(
|
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),
|
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<Settings, bool>(
|
||||||
selector: (context, s) => s.showOverlayInfo,
|
selector: (context, s) => s.showOverlayInfo,
|
||||||
builder: (context, current, child) => SwitchListTile(
|
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<Settings, bool>(
|
||||||
selector: (context, s) => s.showOverlayThumbnailPreview,
|
selector: (context, s) => s.showOverlayThumbnailPreview,
|
||||||
builder: (context, current, child) => SwitchListTile(
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
|
|
@ -30,7 +30,18 @@ class ViewerActionEditorPage extends StatelessWidget {
|
||||||
const ViewerActionEditorPage({Key? key}) : super(key: key);
|
const ViewerActionEditorPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
static const allAvailableActions = [
|
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.rotateCCW,
|
||||||
EntryAction.rotateCW,
|
EntryAction.rotateCW,
|
||||||
EntryAction.flip,
|
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/action/single_entry_editor.dart';
|
||||||
import 'package:aves/widgets/viewer/debug/debug_page.dart';
|
import 'package:aves/widgets/viewer/debug/debug_page.dart';
|
||||||
import 'package:aves/widgets/viewer/info/notifications.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/source_viewer_page.dart';
|
||||||
|
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -99,7 +101,19 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
case EntryAction.viewSource:
|
case EntryAction.viewSource:
|
||||||
_goToSourceViewer(context);
|
_goToSourceViewer(context);
|
||||||
break;
|
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:
|
case EntryAction.edit:
|
||||||
androidAppService.edit(entry.uri, entry.mimeType).then((success) {
|
androidAppService.edit(entry.uri, entry.mimeType).then((success) {
|
||||||
if (!success) showNoMatchingAppDialog(context);
|
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/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.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/entry_vertical_pager.dart';
|
||||||
import 'package:aves/widgets/viewer/hero.dart';
|
import 'package:aves/widgets/viewer/hero.dart';
|
||||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||||
import 'package:aves/widgets/viewer/multipage/controller.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.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/notifications.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/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/page_entry_builder.dart';
|
||||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.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 AChangeNotifier _verticalScrollNotifier = AChangeNotifier();
|
||||||
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
|
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
|
||||||
late AnimationController _overlayAnimationController;
|
late AnimationController _overlayAnimationController;
|
||||||
late Animation<double> _topOverlayScale, _bottomOverlayScale;
|
late Animation<double> _overlayButtonScale, _overlayVideoControlScale;
|
||||||
late Animation<Offset> _bottomOverlayOffset;
|
late Animation<Offset> _overlayTopOffset;
|
||||||
EdgeInsets? _frozenViewInsets, _frozenViewPadding;
|
EdgeInsets? _frozenViewInsets, _frozenViewPadding;
|
||||||
late VideoActionDelegate _videoActionDelegate;
|
late VideoActionDelegate _videoActionDelegate;
|
||||||
final Map<MultiPageController, Future<void> Function()> _multiPageControllerPageListeners = {};
|
final Map<MultiPageController, Future<void> Function()> _multiPageControllerPageListeners = {};
|
||||||
|
@ -110,17 +109,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
||||||
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
_topOverlayScale = CurvedAnimation(
|
_overlayButtonScale = CurvedAnimation(
|
||||||
parent: _overlayAnimationController,
|
parent: _overlayAnimationController,
|
||||||
// a little bounce at the top
|
// a little bounce at the top
|
||||||
curve: Curves.easeOutBack,
|
curve: Curves.easeOutBack,
|
||||||
);
|
);
|
||||||
_bottomOverlayScale = CurvedAnimation(
|
_overlayVideoControlScale = CurvedAnimation(
|
||||||
parent: _overlayAnimationController,
|
parent: _overlayAnimationController,
|
||||||
// no bounce at the bottom, to avoid video controller displacement
|
// no bounce at the bottom, to avoid video controller displacement
|
||||||
curve: Curves.easeOutQuad,
|
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,
|
parent: _overlayAnimationController,
|
||||||
curve: Curves.easeOutQuad,
|
curve: Curves.easeOutQuad,
|
||||||
));
|
));
|
||||||
|
@ -209,7 +208,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
||||||
if (_currentHorizontalPage != index) {
|
if (_currentHorizontalPage != index) {
|
||||||
_horizontalPager.jumpToPage(index);
|
_horizontalPager.jumpToPage(index);
|
||||||
}
|
}
|
||||||
} else if (notification is VideoGestureNotification) {
|
} else if (notification is VideoActionNotification) {
|
||||||
final controller = notification.controller;
|
final controller = notification.controller;
|
||||||
final action = notification.action;
|
final action = notification.action;
|
||||||
_videoActionDelegate.onActionSelected(context, controller, action);
|
_videoActionDelegate.onActionSelected(context, controller, action);
|
||||||
|
@ -245,27 +244,20 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
||||||
Widget child = ValueListenableBuilder<AvesEntry?>(
|
Widget child = ValueListenableBuilder<AvesEntry?>(
|
||||||
valueListenable: _entryNotifier,
|
valueListenable: _entryNotifier,
|
||||||
builder: (context, mainEntry, child) {
|
builder: (context, mainEntry, child) {
|
||||||
if (mainEntry == null) return const SizedBox.shrink();
|
if (mainEntry == null) return const SizedBox();
|
||||||
|
|
||||||
Widget _buildContent({AvesEntry? pageEntry}) {
|
return SlideTransition(
|
||||||
return EmbeddedDataOpener(
|
position: _overlayTopOffset,
|
||||||
entry: mainEntry,
|
|
||||||
child: ViewerTopOverlay(
|
child: ViewerTopOverlay(
|
||||||
|
entries: entries,
|
||||||
|
index: _currentHorizontalPage,
|
||||||
|
hasCollection: hasCollection,
|
||||||
mainEntry: mainEntry,
|
mainEntry: mainEntry,
|
||||||
scale: _topOverlayScale,
|
scale: _overlayButtonScale,
|
||||||
canToggleFavourite: hasCollection,
|
|
||||||
viewInsets: _frozenViewInsets,
|
viewInsets: _frozenViewInsets,
|
||||||
viewPadding: _frozenViewPadding,
|
viewPadding: _frozenViewPadding,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return mainEntry.isMultiPage
|
|
||||||
? PageEntryBuilder(
|
|
||||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
|
||||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
|
||||||
)
|
|
||||||
: _buildContent();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -298,7 +290,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
||||||
Widget child = ValueListenableBuilder<AvesEntry?>(
|
Widget child = ValueListenableBuilder<AvesEntry?>(
|
||||||
valueListenable: _entryNotifier,
|
valueListenable: _entryNotifier,
|
||||||
builder: (context, mainEntry, child) {
|
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;
|
final multiPageController = mainEntry.isMultiPage ? context.read<MultiPageConductor>().getController(mainEntry) : null;
|
||||||
|
|
||||||
Widget? _buildExtraBottomOverlay({AvesEntry? pageEntry}) {
|
Widget? _buildExtraBottomOverlay({AvesEntry? pageEntry}) {
|
||||||
|
@ -311,7 +304,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
||||||
builder: (context, videoController, child) => VideoControlOverlay(
|
builder: (context, videoController, child) => VideoControlOverlay(
|
||||||
entry: targetEntry,
|
entry: targetEntry,
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
scale: _bottomOverlayScale,
|
scale: _overlayVideoControlScale,
|
||||||
onActionSelected: (action) {
|
onActionSelected: (action) {
|
||||||
if (videoController != null) {
|
if (videoController != null) {
|
||||||
_videoActionDelegate.onActionSelected(context, videoController, action);
|
_videoActionDelegate.onActionSelected(context, videoController, action);
|
||||||
|
@ -329,7 +322,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
||||||
} else if (targetEntry.is360) {
|
} else if (targetEntry.is360) {
|
||||||
child = PanoramaOverlay(
|
child = PanoramaOverlay(
|
||||||
entry: targetEntry,
|
entry: targetEntry,
|
||||||
scale: _bottomOverlayScale,
|
scale: _overlayButtonScale,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return child != null
|
return child != null
|
||||||
|
@ -348,21 +341,24 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
||||||
)
|
)
|
||||||
: _buildExtraBottomOverlay();
|
: _buildExtraBottomOverlay();
|
||||||
|
|
||||||
return Column(
|
return TooltipTheme(
|
||||||
|
data: TooltipTheme.of(context).copyWith(
|
||||||
|
preferBelow: false,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (extraBottomOverlay != null) extraBottomOverlay,
|
if (extraBottomOverlay != null) extraBottomOverlay,
|
||||||
SlideTransition(
|
ViewerBottomOverlay(
|
||||||
position: _bottomOverlayOffset,
|
|
||||||
child: ViewerBottomOverlay(
|
|
||||||
entries: entries,
|
entries: entries,
|
||||||
index: _currentHorizontalPage,
|
index: _currentHorizontalPage,
|
||||||
showPosition: hasCollection,
|
hasCollection: hasCollection,
|
||||||
|
animationController: _overlayAnimationController,
|
||||||
viewInsets: _frozenViewInsets,
|
viewInsets: _frozenViewInsets,
|
||||||
viewPadding: _frozenViewPadding,
|
viewPadding: _frozenViewPadding,
|
||||||
multiPageController: multiPageController,
|
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/multipage.dart';
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||||
import 'package:aves/model/settings/settings.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/durations.dart';
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/multipage/controller.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:decorated_icon/decorated_icon.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -21,7 +24,98 @@ const double _iconSize = 16.0;
|
||||||
const double _interRowPadding = 2.0;
|
const double _interRowPadding = 2.0;
|
||||||
const double _subRowMinWidth = 300.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 AvesEntry pageEntry;
|
||||||
final OverlayMetadata? details;
|
final OverlayMetadata? details;
|
||||||
final String? position;
|
final String? position;
|
||||||
|
@ -30,7 +124,7 @@ class ViewerDetailOverlay extends StatelessWidget {
|
||||||
|
|
||||||
static const padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
|
static const padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
|
||||||
|
|
||||||
const ViewerDetailOverlay({
|
const ViewerDetailOverlayContent({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.pageEntry,
|
required this.pageEntry,
|
||||||
required this.details,
|
required this.details,
|
||||||
|
@ -46,7 +140,14 @@ class ViewerDetailOverlay extends StatelessWidget {
|
||||||
final hasShootingDetails = details != null && !details!.isEmpty && settings.showOverlayShootingDetails;
|
final hasShootingDetails = details != null && !details!.isEmpty && settings.showOverlayShootingDetails;
|
||||||
final animationDuration = context.select<DurationsData, Duration>((v) => v.viewerOverlayChangeAnimation);
|
final animationDuration = context.select<DurationsData, Duration>((v) => v.viewerOverlayChangeAnimation);
|
||||||
|
|
||||||
return Padding(
|
return DefaultTextStyle(
|
||||||
|
style: Theme.of(context).textTheme.bodyText2!.copyWith(
|
||||||
|
shadows: Constants.embossShadows,
|
||||||
|
),
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
child: Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Selector<MediaQueryData, Orientation>(
|
child: Selector<MediaQueryData, Orientation>(
|
||||||
selector: (context, mq) => mq.orientation,
|
selector: (context, mq) => mq.orientation,
|
||||||
|
@ -59,7 +160,6 @@ class ViewerDetailOverlay extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (positionTitle.isNotEmpty) positionTitle,
|
if (positionTitle.isNotEmpty) positionTitle,
|
||||||
_buildSoloLocationRow(animationDuration),
|
|
||||||
if (twoColumns)
|
if (twoColumns)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: _interRowPadding),
|
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||||
|
@ -86,10 +186,12 @@ class ViewerDetailOverlay extends StatelessWidget {
|
||||||
),
|
),
|
||||||
_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:aves/widgets/viewer/video/controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -17,11 +17,11 @@ class ViewEntryNotification extends Notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class VideoGestureNotification extends Notification {
|
class VideoActionNotification extends Notification {
|
||||||
final AvesVideoController controller;
|
final AvesVideoController controller;
|
||||||
final VideoAction action;
|
final EntryAction action;
|
||||||
|
|
||||||
const VideoGestureNotification({
|
const VideoActionNotification({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.action,
|
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/entry.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/widgets/common/fx/blurred.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/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/common.dart';
|
||||||
|
import 'package:aves/widgets/viewer/overlay/details.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/minimap.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/page_entry_builder.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/conductor.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/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ViewerTopOverlay extends StatelessWidget {
|
class ViewerTopOverlay extends StatelessWidget {
|
||||||
|
final List<AvesEntry> entries;
|
||||||
|
final int index;
|
||||||
final AvesEntry mainEntry;
|
final AvesEntry mainEntry;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final EdgeInsets? viewInsets, viewPadding;
|
final EdgeInsets? viewInsets, viewPadding;
|
||||||
final bool canToggleFavourite;
|
final bool hasCollection;
|
||||||
|
|
||||||
static const double outerPadding = 8;
|
|
||||||
static const double innerPadding = 8;
|
|
||||||
|
|
||||||
const ViewerTopOverlay({
|
const ViewerTopOverlay({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
required this.entries,
|
||||||
|
required this.index,
|
||||||
required this.mainEntry,
|
required this.mainEntry,
|
||||||
required this.scale,
|
required this.scale,
|
||||||
required this.canToggleFavourite,
|
required this.hasCollection,
|
||||||
required this.viewInsets,
|
required this.viewInsets,
|
||||||
required this.viewPadding,
|
required this.viewPadding,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
late Widget child;
|
||||||
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();
|
|
||||||
|
|
||||||
return mainEntry.isMultiPage
|
if (mainEntry.isMultiPage) {
|
||||||
? PageEntryBuilder(
|
final multiPageController = context.read<MultiPageConductor>().getController(mainEntry);
|
||||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
child = PageEntryBuilder(
|
||||||
builder: (pageEntry) => _buildOverlay(context, availableCount, mainEntry, pageEntry: pageEntry),
|
multiPageController: multiPageController,
|
||||||
)
|
builder: (pageEntry) => _buildOverlay(
|
||||||
: _buildOverlay(context, availableCount, mainEntry);
|
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 {
|
} else {
|
||||||
final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry! : mainEntry;
|
child = _buildOverlay(context, 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>(
|
return child;
|
||||||
selector: (context, s) => s.isRotationLocked,
|
}
|
||||||
builder: (context, s, child) {
|
|
||||||
final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(_isVisible).take(availableCount - 1).toList();
|
Widget _buildOverlay(
|
||||||
final topLevelActions = EntryActions.topLevel.where((action) => !quickActions.contains(action)).where(_isVisible).toList();
|
BuildContext context,
|
||||||
final exportActions = EntryActions.export.where((action) => !quickActions.contains(action)).where(_isVisible).toList();
|
AvesEntry mainEntry, {
|
||||||
return _TopOverlayRow(
|
AvesEntry? pageEntry,
|
||||||
quickActions: quickActions,
|
MultiPageController? multiPageController,
|
||||||
topLevelActions: topLevelActions,
|
}) {
|
||||||
exportActions: exportActions,
|
pageEntry ??= mainEntry;
|
||||||
scale: scale,
|
|
||||||
mainEntry: mainEntry,
|
final showInfo = settings.showOverlayInfo;
|
||||||
pageEntry: pageEntry!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (settings.showOverlayMinimap) {
|
|
||||||
final viewStateConductor = context.read<ViewStateConductor>();
|
final viewStateConductor = context.read<ViewStateConductor>();
|
||||||
final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry);
|
final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry);
|
||||||
|
|
||||||
|
final blurred = settings.enableOverlayBlurEffect;
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buttonRow,
|
if (showInfo)
|
||||||
const SizedBox(height: 8),
|
BlurredRect(
|
||||||
FadeTransition(
|
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,
|
opacity: scale,
|
||||||
child: Minimap(
|
child: Minimap(
|
||||||
viewStateNotifier: viewStateNotifier,
|
viewStateNotifier: viewStateNotifier,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return buttonRow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TopOverlayRow extends StatelessWidget {
|
|
||||||
final List<EntryAction> quickActions, topLevelActions, exportActions;
|
|
||||||
final Animation<double> scale;
|
|
||||||
final AvesEntry mainEntry, pageEntry;
|
|
||||||
|
|
||||||
AvesEntry get favouriteTargetEntry => mainEntry.isBurst ? pageEntry : mainEntry;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final hasOverflowMenu = pageEntry.canRotateAndFlip || topLevelActions.isNotEmpty || exportActions.isNotEmpty;
|
|
||||||
return Row(
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 '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/enums/enums.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
@ -15,7 +15,7 @@ import 'package:provider/provider.dart';
|
||||||
class VideoControlRow extends StatelessWidget {
|
class VideoControlRow extends StatelessWidget {
|
||||||
final AvesVideoController? controller;
|
final AvesVideoController? controller;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final Function(VideoAction value) onActionSelected;
|
final Function(EntryAction value) onActionSelected;
|
||||||
|
|
||||||
static const double padding = 8;
|
static const double padding = 8;
|
||||||
static const Radius radius = Radius.circular(123);
|
static const Radius radius = Radius.circular(123);
|
||||||
|
@ -41,7 +41,7 @@ class VideoControlRow extends StatelessWidget {
|
||||||
child: _buildOverlayButton(
|
child: _buildOverlayButton(
|
||||||
child: PlayToggler(
|
child: PlayToggler(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onPressed: () => onActionSelected(VideoAction.togglePlay),
|
onPressed: () => onActionSelected(EntryAction.videoTogglePlay),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -52,30 +52,28 @@ class VideoControlRow extends StatelessWidget {
|
||||||
const SizedBox(width: padding),
|
const SizedBox(width: padding),
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
context,
|
context,
|
||||||
VideoAction.replay10,
|
EntryAction.videoReplay10,
|
||||||
borderRadius: const BorderRadius.only(topLeft: radius, bottomLeft: radius),
|
borderRadius: const BorderRadius.only(topLeft: radius, bottomLeft: radius),
|
||||||
),
|
),
|
||||||
_buildOverlayButton(
|
_buildOverlayButton(
|
||||||
child: PlayToggler(
|
child: PlayToggler(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onPressed: () => onActionSelected(VideoAction.togglePlay),
|
onPressed: () => onActionSelected(EntryAction.videoTogglePlay),
|
||||||
),
|
),
|
||||||
borderRadius: const BorderRadius.all(Radius.zero),
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
),
|
),
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
context,
|
context,
|
||||||
VideoAction.skip10,
|
EntryAction.videoSkip10,
|
||||||
borderRadius: const BorderRadius.only(topRight: radius, bottomRight: radius),
|
borderRadius: const BorderRadius.only(topRight: radius, bottomRight: radius),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
case VideoControls.playOutside:
|
case VideoControls.playOutside:
|
||||||
|
final trashed = controller?.entry.trashed ?? false;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(start: padding),
|
padding: const EdgeInsetsDirectional.only(start: padding),
|
||||||
child: _buildIconButton(
|
child: _buildIconButton(context, EntryAction.open, enabled: !trashed),
|
||||||
context,
|
|
||||||
VideoAction.playOutside,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -94,14 +92,15 @@ class VideoControlRow extends StatelessWidget {
|
||||||
|
|
||||||
Widget _buildIconButton(
|
Widget _buildIconButton(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
VideoAction action, {
|
EntryAction action, {
|
||||||
|
bool enabled = true,
|
||||||
BorderRadius? borderRadius,
|
BorderRadius? borderRadius,
|
||||||
}) =>
|
}) =>
|
||||||
_buildOverlayButton(
|
_buildOverlayButton(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: action.getIcon(),
|
icon: action.getIcon(),
|
||||||
onPressed: () => onActionSelected(action),
|
onPressed: enabled ? () => onActionSelected(action) : null,
|
||||||
tooltip: action.getText(context),
|
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 '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/filters/album.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/source/collection_lens.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/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/size_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/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_speed_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart';
|
||||||
import 'package:aves/widgets/settings/video/video.dart';
|
import 'package:aves/widgets/settings/video/video.dart';
|
||||||
|
@ -34,37 +35,41 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
stopOverlayHidingTimer();
|
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
|
// make sure overlay is not disappearing when selecting an action
|
||||||
stopOverlayHidingTimer();
|
stopOverlayHidingTimer();
|
||||||
const ToggleOverlayNotification(visible: true).dispatch(context);
|
const ToggleOverlayNotification(visible: true).dispatch(context);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case VideoAction.captureFrame:
|
case EntryAction.videoCaptureFrame:
|
||||||
_captureFrame(context, controller);
|
_captureFrame(context, controller);
|
||||||
break;
|
break;
|
||||||
case VideoAction.playOutside:
|
case EntryAction.videoSelectStreams:
|
||||||
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:
|
|
||||||
_showStreamSelectionDialog(context, controller);
|
_showStreamSelectionDialog(context, controller);
|
||||||
break;
|
break;
|
||||||
case VideoAction.setSpeed:
|
case EntryAction.videoSetSpeed:
|
||||||
_showSpeedDialog(context, controller);
|
_showSpeedDialog(context, controller);
|
||||||
break;
|
break;
|
||||||
case VideoAction.settings:
|
case EntryAction.videoSettings:
|
||||||
_showSettings(context, controller);
|
_showSettings(context, controller);
|
||||||
break;
|
break;
|
||||||
case VideoAction.togglePlay:
|
case EntryAction.videoTogglePlay:
|
||||||
_togglePlayPause(context, controller);
|
_togglePlayPause(context, controller);
|
||||||
break;
|
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 '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.dart';
|
||||||
import 'package:aves/model/entry_images.dart';
|
import 'package:aves/model/entry_images.dart';
|
||||||
import 'package:aves/model/settings/enums/accessibility_animations.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();
|
if (videoController == null) return const SizedBox();
|
||||||
|
|
||||||
Positioned _buildDoubleTapDetector(
|
Positioned _buildDoubleTapDetector(
|
||||||
VideoAction action, {
|
EntryAction action, {
|
||||||
double widthFactor = 1,
|
double widthFactor = 1,
|
||||||
AlignmentGeometry alignment = Alignment.center,
|
AlignmentGeometry alignment = Alignment.center,
|
||||||
IconData? Function()? icon,
|
IconData? Function()? icon,
|
||||||
|
@ -215,7 +215,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
VideoGestureNotification(
|
VideoActionNotification(
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
action: action,
|
action: action,
|
||||||
).dispatch(context);
|
).dispatch(context);
|
||||||
|
@ -256,12 +256,12 @@ class _EntryPageViewState extends State<EntryPageView> {
|
||||||
),
|
),
|
||||||
if (playGesture)
|
if (playGesture)
|
||||||
_buildDoubleTapDetector(
|
_buildDoubleTapDetector(
|
||||||
VideoAction.togglePlay,
|
EntryAction.videoTogglePlay,
|
||||||
icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
|
icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
|
||||||
),
|
),
|
||||||
if (seekGesture) ...[
|
if (seekGesture) ...[
|
||||||
_buildDoubleTapDetector(VideoAction.replay10, widthFactor: .25, alignment: Alignment.centerLeft),
|
_buildDoubleTapDetector(EntryAction.videoReplay10, widthFactor: .25, alignment: Alignment.centerLeft),
|
||||||
_buildDoubleTapDetector(VideoAction.skip10, widthFactor: .25, alignment: Alignment.centerRight),
|
_buildDoubleTapDetector(EntryAction.videoSkip10, widthFactor: .25, alignment: Alignment.centerRight),
|
||||||
],
|
],
|
||||||
if (useActionGesture)
|
if (useActionGesture)
|
||||||
ValueListenableBuilder<Widget?>(
|
ValueListenableBuilder<Widget?>(
|
||||||
|
|
Loading…
Reference in a new issue