diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b5401ad85..8846555de 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -590,6 +590,10 @@ "@settingsVideoLoopModeTile": {}, "settingsVideoLoopModeTitle": "Loop Mode", "@settingsVideoLoopModeTitle": {}, + "settingsVideoQuickActionsTile": "Quick video actions", + "@settingsVideoQuickActionsTile": {}, + "settingsVideoQuickActionEditorTitle": "Quick Video Actions", + "@settingsVideoQuickActionEditorTitle": {}, "settingsSectionPrivacy": "Privacy", "@settingsSectionPrivacy": {}, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index a5fa827e4..336d3f98c 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -278,6 +278,8 @@ "settingsVideoEnableAutoPlay": "자동 재생", "settingsVideoLoopModeTile": "반복 모드", "settingsVideoLoopModeTitle": "반복 모드", + "settingsVideoQuickActionsTile": "빠른 동영상 작업", + "settingsVideoQuickActionEditorTitle": "빠른 동영상 작업", "settingsSectionPrivacy": "개인정보 보호", "settingsEnableAnalytics": "진단 데이터 보내기", diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index 3241334a6..def93f0c1 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -43,6 +43,7 @@ class DebugSettingsSection extends StatelessWidget { 'tileExtent - Tags': '${settings.getTileExtent(TagListPage.routeName)}', 'infoMapZoom': '${settings.infoMapZoom}', 'viewerQuickActions': '${settings.viewerQuickActions}', + 'videoQuickActions': '${settings.videoQuickActions}', 'pinnedFilters': toMultiline(settings.pinnedFilters), 'hiddenFilters': toMultiline(settings.hiddenFilters), 'searchHistory': toMultiline(settings.searchHistory), diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index 5087323ca..f4109cdf6 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.dart @@ -47,6 +47,7 @@ class _VideoStreamSelectionDialogState extends State final canSelect = canSelectVideo || canSelectAudio || canSelectText; return AvesDialog( context: context, + content: canSelect ? null : Text(context.l10n.videoStreamSelectionDialogNoSelection), scrollableContent: canSelect ? [ if (canSelectVideo) @@ -75,22 +76,17 @@ class _VideoStreamSelectionDialogState extends State ), const SizedBox(height: 8), ] - : [ - Container( - alignment: Alignment.center, - padding: const EdgeInsets.all(16), - child: Text(context.l10n.videoStreamSelectionDialogNoSelection), - ), - ], + : null, actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), - TextButton( - onPressed: () => _submit(context), - child: Text(context.l10n.applyButtonLabel), - ), + if (canSelect) + TextButton( + onPressed: () => _submit(context), + child: Text(context.l10n.applyButtonLabel), + ), ], ); } diff --git a/lib/widgets/settings/quick_actions/editor_page.dart b/lib/widgets/settings/quick_actions/editor_page.dart index 4f830ef31..14750be85 100644 --- a/lib/widgets/settings/quick_actions/editor_page.dart +++ b/lib/widgets/settings/quick_actions/editor_page.dart @@ -16,7 +16,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class QuickActionEditorPage extends StatefulWidget { - final String bannerText; + final String title, bannerText; final List allAvailableActions; final IconData? Function(T action) actionIcon; final String Function(BuildContext context, T action) actionText; @@ -24,6 +24,7 @@ class QuickActionEditorPage extends StatefulWidget { final void Function(List actions) save; const QuickActionEditorPage({ + required this.title, required this.bannerText, required this.allAvailableActions, required this.actionIcon, @@ -96,7 +97,7 @@ class _QuickActionEditorPageState extends State { expandedNotifier: _expandedNotifier, showHighlight: false, children: [ - QuickEntryActionsTile(), + ViewerActionsTile(), SwitchListTile( value: settings.showOverlayMinimap, onChanged: (v) => settings.showOverlayMinimap = v, @@ -267,6 +268,7 @@ class _SettingsPageState extends State { } }, ), + VideoActionsTile(), ], ); } diff --git a/lib/widgets/settings/video_actions_editor.dart b/lib/widgets/settings/video_actions_editor.dart new file mode 100644 index 000000000..02b5471ea --- /dev/null +++ b/lib/widgets/settings/video_actions_editor.dart @@ -0,0 +1,42 @@ +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/quick_actions/editor_page.dart'; +import 'package:flutter/material.dart'; + +class VideoActionsTile extends StatelessWidget { + @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( + title: context.l10n.settingsVideoQuickActionEditorTitle, + bannerText: context.l10n.settingsViewerQuickActionEditorBanner, + allAvailableActions: VideoActions.all, + actionIcon: (action) => action.getIcon(), + actionText: (context, action) => action.getText(context), + load: () => settings.videoQuickActions.toList(), + save: (actions) => settings.videoQuickActions = actions, + ); + } +} diff --git a/lib/widgets/settings/entry_actions_editor.dart b/lib/widgets/settings/viewer_actions_editor.dart similarity index 75% rename from lib/widgets/settings/entry_actions_editor.dart rename to lib/widgets/settings/viewer_actions_editor.dart index c60dba463..630d0a37d 100644 --- a/lib/widgets/settings/entry_actions_editor.dart +++ b/lib/widgets/settings/viewer_actions_editor.dart @@ -4,7 +4,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; -class QuickEntryActionsTile extends StatelessWidget { +class ViewerActionsTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( @@ -13,8 +13,8 @@ class QuickEntryActionsTile extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - settings: const RouteSettings(name: QuickEntryActionEditorPage.routeName), - builder: (context) => const QuickEntryActionEditorPage(), + settings: const RouteSettings(name: ViewerActionEditorPage.routeName), + builder: (context) => const ViewerActionEditorPage(), ), ); }, @@ -22,10 +22,10 @@ class QuickEntryActionsTile extends StatelessWidget { } } -class QuickEntryActionEditorPage extends StatelessWidget { - static const routeName = '/settings/quick_entry_actions'; +class ViewerActionEditorPage extends StatelessWidget { + static const routeName = '/settings/viewer_actions'; - const QuickEntryActionEditorPage({Key? key}) : super(key: key); + const ViewerActionEditorPage({Key? key}) : super(key: key); static const allAvailableActions = [ EntryAction.info, @@ -44,6 +44,7 @@ class QuickEntryActionEditorPage extends StatelessWidget { @override Widget build(BuildContext context) { return QuickActionEditorPage( + title: context.l10n.settingsViewerQuickActionEditorTitle, bannerText: context.l10n.settingsViewerQuickActionEditorBanner, allAvailableActions: allAvailableActions, actionIcon: (action) => action.getIcon(), diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart index 734e15058..4c90a38f4 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video.dart @@ -6,6 +6,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -18,6 +19,7 @@ import 'package:aves/widgets/viewer/overlay/notifications.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; class VideoControlOverlay extends StatefulWidget { final AvesEntry entry; @@ -51,6 +53,9 @@ class _VideoControlOverlayState extends State with SingleTi bool get isPlaying => controller?.isPlaying ?? false; + static const double outerPadding = 8; + static const double innerPadding = 8; + @override Widget build(BuildContext context) { return StreamBuilder( @@ -58,10 +63,11 @@ class _VideoControlOverlayState extends State with SingleTi builder: (context, snapshot) { // do not use stream snapshot because it is obsolete when switching between videos final status = controller?.status ?? VideoStatus.idle; - List children; + Widget child; if (status == VideoStatus.error) { - children = [ - OverlayButton( + child = Align( + alignment: AlignmentDirectional.centerEnd, + child: OverlayButton( scale: scale, child: IconButton( icon: const Icon(AIcons.openOutside), @@ -69,32 +75,38 @@ class _VideoControlOverlayState extends State with SingleTi tooltip: context.l10n.viewerOpenTooltip, ), ), - ]; + ); } else { - final quickActions = settings.videoQuickActions; - final menuActions = VideoActions.all.where((action) => !quickActions.contains(action)).toList(); - children = [ - Expanded( - child: _buildProgressBar(), - ), - const SizedBox(width: 8), - _ButtonRow( - quickActions: quickActions, - menuActions: menuActions, - scale: scale, - controller: controller, - ), - ]; + child = Selector( + selector: (c, mq) => mq.size.width - mq.padding.horizontal, + builder: (c, mqWidth, child) { + final buttonWidth = OverlayButton.getSize(context); + final availableCount = ((mqWidth - outerPadding * 2) / (buttonWidth + innerPadding)).floor(); + final quickActions = settings.videoQuickActions.take(availableCount - 1).toList(); + final menuActions = VideoActions.all.where((action) => !quickActions.contains(action)).toList(); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _ButtonRow( + quickActions: quickActions, + menuActions: menuActions, + scale: scale, + controller: controller, + ), + const SizedBox(height: 8), + _buildProgressBar(), + ], + ); + }, + ); } return TooltipTheme( data: TooltipTheme.of(context).copyWith( preferBelow: false, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: children, - ), + child: child, ); }); } @@ -136,10 +148,16 @@ class _VideoControlOverlayState extends State with SingleTi builder: (context, snapshot) { // do not use stream snapshot because it is obsolete when switching between videos final position = controller?.currentPosition.floor() ?? 0; - return Text(formatFriendlyDuration(Duration(milliseconds: position))); + return Text( + formatFriendlyDuration(Duration(milliseconds: position)), + style: const TextStyle(shadows: Constants.embossShadows), + ); }), const Spacer(), - Text(entry.durationText), + Text( + entry.durationText, + style: const TextStyle(shadows: Constants.embossShadows), + ), ], ), ClipRRect( @@ -196,16 +214,20 @@ class _ButtonRow extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ...quickActions.map((action) => _buildOverlayButton(context, action)), - OverlayButton( - scale: scale, - child: PopupMenuButton( - itemBuilder: (context) => menuActions.map((action) => _buildPopupMenuItem(context, action)).toList(), - onSelected: (action) { - // wait for the popup menu to hide before proceeding with the action - Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action)); - }, + if (menuActions.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(start: padding), + child: OverlayButton( + scale: scale, + child: PopupMenuButton( + itemBuilder: (context) => menuActions.map((action) => _buildPopupMenuItem(context, action)).toList(), + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action)); + }, + ), + ), ), - ), ], ); } @@ -232,7 +254,7 @@ class _ButtonRow extends StatelessWidget { break; } return Padding( - padding: const EdgeInsetsDirectional.only(end: padding), + padding: const EdgeInsetsDirectional.only(start: padding), child: OverlayButton( scale: scale, child: child, diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index 03b62c557..b24588bde 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -34,8 +34,8 @@ class OverlayButton extends StatelessWidget { ); } - // icon (24) + icon padding (8) + button padding (16) + border (2) - static double getSize(BuildContext context) => 50.0; + // icon (24) + icon padding (8) + button padding (16) + border (1 or 2) + static double getSize(BuildContext context) => 48.0 + AvesBorder.borderWidth * 2; } class OverlayTextButton extends StatelessWidget { diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 4e20982a2..90b79da2e 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -25,7 +25,8 @@ class ViewerTopOverlay extends StatelessWidget { final bool canToggleFavourite; final ValueNotifier? viewStateNotifier; - static const double padding = 8; + static const double outerPadding = 8; + static const double innerPadding = 8; const ViewerTopOverlay({ Key? key, @@ -43,11 +44,12 @@ class ViewerTopOverlay extends StatelessWidget { return SafeArea( minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero), child: Padding( - padding: const EdgeInsets.all(padding), + padding: const EdgeInsets.all(outerPadding), child: Selector( selector: (c, mq) => mq.size.width - mq.padding.horizontal, builder: (c, mqWidth, child) { - final availableCount = (mqWidth / (OverlayButton.getSize(context) + padding)).floor() - 2; + final buttonWidth = OverlayButton.getSize(context); + final availableCount = ((mqWidth - outerPadding * 2 - buttonWidth) / (buttonWidth + innerPadding)).floor(); Widget? child; if (mainEntry.isMultiPage) { @@ -108,7 +110,7 @@ class ViewerTopOverlay extends StatelessWidget { } } - final quickActions = settings.viewerQuickActions.where(_canDo).take(availableCount).toList(); + final quickActions = settings.viewerQuickActions.where(_canDo).take(availableCount - 1).toList(); final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList(); final externalAppActions = EntryActions.externalApp.where(_canDo).toList(); final buttonRow = _TopOverlayRow( @@ -157,8 +159,6 @@ class _TopOverlayRow extends StatelessWidget { required this.onActionSelected, }) : super(key: key); - static const double padding = 8; - @override Widget build(BuildContext context) { return Row( @@ -228,7 +228,7 @@ class _TopOverlayRow extends StatelessWidget { } return child != null ? Padding( - padding: const EdgeInsetsDirectional.only(end: padding), + padding: const EdgeInsetsDirectional.only(end: ViewerTopOverlay.innerPadding), child: OverlayButton( scale: scale, child: child,