From b343e32db09598b05718b1d91e0531ebf4805a76 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 5 Jan 2023 17:15:00 +0100 Subject: [PATCH] tv layout on non-tv devices --- .../aves/channel/calls/MediaSessionHandler.kt | 15 ++- lib/l10n/app_en.arb | 2 + lib/model/settings/defaults.dart | 1 + lib/model/settings/settings.dart | 13 +- lib/widgets/about/about_page.dart | 15 ++- lib/widgets/aves_app.dart | 28 +++- lib/widgets/collection/app_bar.dart | 123 +++++++++--------- lib/widgets/collection/collection_grid.dart | 5 +- lib/widgets/collection/collection_page.dart | 3 +- .../collection/entry_set_action_delegate.dart | 4 +- lib/widgets/collection/filter_bar.dart | 23 ++-- lib/widgets/common/basic/color_list_tile.dart | 8 +- lib/widgets/common/basic/insets.dart | 4 +- .../common/behaviour/pop/tv_navigation.dart | 3 +- lib/widgets/common/grid/header.dart | 4 +- lib/widgets/common/identity/aves_app_bar.dart | 3 +- lib/widgets/common/map/buttons/panel.dart | 3 +- lib/widgets/common/search/delegate.dart | 4 +- .../common/action_delegates/chip_set.dart | 3 +- lib/widgets/filter_grids/common/app_bar.dart | 81 ++++++------ .../filter_grids/common/filter_grid_page.dart | 7 +- lib/widgets/navigation/tv_rail.dart | 39 +++--- .../settings/accessibility/accessibility.dart | 3 +- lib/widgets/settings/display/display.dart | 46 ++++++- .../settings/navigation/navigation.dart | 9 +- lib/widgets/settings/privacy/privacy.dart | 6 +- lib/widgets/settings/settings_page.dart | 4 +- .../settings/thumbnails/thumbnails.dart | 4 +- lib/widgets/settings/video/video.dart | 3 +- lib/widgets/settings/viewer/overlay.dart | 7 +- lib/widgets/settings/viewer/viewer.dart | 9 +- lib/widgets/stats/stats_page.dart | 5 +- .../viewer/action/entry_action_delegate.dart | 6 +- lib/widgets/viewer/entry_vertical_pager.dart | 11 +- lib/widgets/viewer/entry_viewer_stack.dart | 4 +- lib/widgets/viewer/info/info_app_bar.dart | 4 +- lib/widgets/viewer/overlay/bottom.dart | 3 +- .../viewer/overlay/slideshow_buttons.dart | 6 +- .../viewer/overlay/viewer_buttons.dart | 3 +- lib/widgets/viewer/panorama_page.dart | 4 +- lib/widgets/welcome_page.dart | 3 +- untranslated.json | 85 ++++++++++-- 42 files changed, 379 insertions(+), 237 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt index f2713b79c..e96903269 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt @@ -1,16 +1,21 @@ package deckers.thibault.aves.channel.calls +import android.content.ComponentName import android.content.Context +import android.content.Intent import android.media.session.PlaybackState import android.net.Uri import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log +import android.view.KeyEvent +import androidx.media.session.MediaButtonReceiver import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.getParcelableExtraCompat import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -74,7 +79,9 @@ class MediaSessionHandler(private val context: Context) : MethodCallHandler { var session = sessions[uri] if (session == null) { - session = MediaSessionCompat(context, "aves-$uri") + val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE) + val mbrName = ComponentName(context, MediaButtonReceiver::class.java) + session = MediaSessionCompat(context, "aves-$uri", mbrName, mbrIntent) sessions[uri] = session val metadata = MediaMetadataCompat.Builder() @@ -86,6 +93,12 @@ class MediaSessionHandler(private val context: Context) : MethodCallHandler { session.setMetadata(metadata) val callback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() { + override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { + val keyEvent = mediaButtonEvent.getParcelableExtraCompat(Intent.EXTRA_KEY_EVENT) ?: return false + Log.d(LOG_TAG, "TLAD onMediaButtonEvent keyEvent=$keyEvent") + return super.onMediaButtonEvent(mediaButtonEvent) + } + override fun onPlay() { super.onPlay() Log.d(LOG_TAG, "TLAD onPlay uri=$uri") diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6c9d93587..49356110a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -656,6 +656,7 @@ "settingsSystemDefault": "System default", "settingsDefault": "Default", "settingsDisabled": "Disabled", + "settingsModificationWarningDialogMessage": "Other settings will be modified.", "settingsSearchFieldLabel": "Search settings", "settingsSearchEmpty": "No matching setting", @@ -816,6 +817,7 @@ "settingsThemeEnableDynamicColor": "Dynamic color", "settingsDisplayRefreshRateModeTile": "Display refresh rate", "settingsDisplayRefreshRateModeDialogTitle": "Refresh Rate", + "settingsDisplayUseTvInterface": "Android TV interface", "settingsLanguageSectionTitle": "Language & Formats", "settingsLanguageTile": "Language", diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 9317cd1f0..2d6716e6d 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -24,6 +24,7 @@ class SettingsDefaults { static const themeColorMode = AvesThemeColorMode.polychrome; static const enableDynamicColor = false; static const enableBlurEffect = true; // `enableBlurEffect` has a contextual default value + static const forceTvLayout = false; // navigation static const mustBackTwiceToExit = true; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 124408268..36d808f75 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -70,6 +70,7 @@ class Settings extends ChangeNotifier { static const themeColorModeKey = 'theme_color_mode'; static const enableDynamicColorKey = 'dynamic_color'; static const enableBlurEffectKey = 'enable_overlay_blur_effect'; + static const forceTvLayoutKey = 'force_tv_layout'; // navigation static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; @@ -240,7 +241,11 @@ class Settings extends ChangeNotifier { } } - if (device.isTelevision) { + applyTvSettings(); + } + + void applyTvSettings() { + if (settings.useTvLayout) { themeBrightness = AvesThemeBrightness.dark; mustBackTwiceToExit = false; // address `TV-BU` / `TV-BY` requirements from https://developer.android.com/docs/quality-guidelines/tv-app-quality @@ -392,6 +397,12 @@ class Settings extends ChangeNotifier { set enableBlurEffect(bool newValue) => setAndNotify(enableBlurEffectKey, newValue); + bool get forceTvLayout => getBool(forceTvLayoutKey) ?? SettingsDefaults.forceTvLayout; + + set forceTvLayout(bool newValue) => setAndNotify(forceTvLayoutKey, newValue); + + bool get useTvLayout => device.isTelevision || forceTvLayout; + // navigation bool get mustBackTwiceToExit => getBool(mustBackTwiceToExitKey) ?? SettingsDefaults.mustBackTwiceToExit; diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index a4d1c18ef..ae2871825 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/about/app_ref.dart'; import 'package:aves/widgets/about/bug_report.dart'; import 'package:aves/widgets/about/credits.dart'; @@ -28,7 +28,7 @@ class AboutPage extends StatelessWidget { delegate: SliverChildListDelegate( [ const AppReference(), - if (!device.isTelevision) ...[ + if (!settings.useTvLayout) ...[ const Divider(), const BugReport(), ], @@ -46,7 +46,8 @@ class AboutPage extends StatelessWidget { ], ); - if (device.isTelevision) { + if (settings.useTvLayout) { + final isRtl = context.isRtl; return Scaffold( body: AvesPopScope( handlers: const [TvNavigationPopHandler.pop], @@ -55,7 +56,13 @@ class AboutPage extends StatelessWidget { TvRail( controller: context.read(), ), - Expanded(child: body), + Expanded( + child: SafeArea( + left: isRtl, + right: !isRtl, + child: body, + ), + ), ], ), ), diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 116d693ae..d5c139982 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -416,17 +416,20 @@ class _AvesAppState extends State with WidgetsBindingObserver { final stopwatch = Stopwatch()..start(); await device.init(); - if (device.isTelevision) { + await mobileServices.init(); + await settings.init(monitorPlatformSettings: true); + settings.isRotationLocked = await windowService.isRotationLocked(); + settings.areAnimationsRemoved = await AccessibilityService.areAnimationsRemoved(); + if (settings.useTvLayout) { _pageTransitionsBuilderNotifier.value = const TvPageTransitionsBuilder(); _tvMediaQueryModifierNotifier.value = (mq) => mq.copyWith( textScaleFactor: 1.1, navigationMode: NavigationMode.directional, ); + if (settings.forceTvLayout) { + await windowService.requestOrientation(Orientation.landscape); + } } - await mobileServices.init(); - await settings.init(monitorPlatformSettings: true); - settings.isRotationLocked = await windowService.isRotationLocked(); - settings.areAnimationsRemoved = await AccessibilityService.areAnimationsRemoved(); _monitorSettings(); FijkLog.setLevel(FijkLogLevel.Warn); @@ -448,15 +451,28 @@ class _AvesAppState extends State with WidgetsBindingObserver { void applyKeepScreenOn() => settings.keepScreenOn.apply(); void applyIsRotationLocked() { - if (!settings.isRotationLocked) { + if (!settings.isRotationLocked && !settings.useTvLayout) { windowService.requestOrientation(); } } + void applyForceTvLayout() { + settings.applyTvSettings(); + windowService.requestOrientation(settings.forceTvLayout ? Orientation.landscape : null); + AvesApp.navigatorKey.currentState!.pushAndRemoveUntil( + MaterialPageRoute( + settings: const RouteSettings(name: HomePage.routeName), + builder: (_) => _getFirstPage(), + ), + (route) => false, + ); + } + settings.updateStream.where((event) => event.key == Settings.isInstalledAppAccessAllowedKey).listen((_) => applyIsInstalledAppAccessAllowed()); settings.updateStream.where((event) => event.key == Settings.displayRefreshRateModeKey).listen((_) => applyDisplayRefreshRateMode()); settings.updateStream.where((event) => event.key == Settings.keepScreenOnKey).listen((_) => applyKeepScreenOn()); settings.updateStream.where((event) => event.key == Settings.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked()); + settings.updateStream.where((event) => event.key == Settings.forceTvLayoutKey).listen((_) => applyForceTvLayout()); applyDisplayRefreshRateMode(); applyKeepScreenOn(); diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index c90e28aa0..f767e9c92 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -151,70 +151,75 @@ class _CollectionAppBarState extends State with SingleTickerPr final selection = context.watch>(); final isSelecting = selection.isSelecting; _isSelectingNotifier.value = isSelecting; - return AnimatedBuilder( - animation: collection.filterChangeNotifier, - builder: (context, child) { - final removableFilters = appMode != AppMode.pickMediaInternal; - return Selector( - selector: (context, query) => query.enabled, - builder: (context, queryEnabled, child) { - return Selector>( - selector: (context, s) => s.collectionBrowsingQuickActions, - builder: (context, _, child) { - final isTelevision = device.isTelevision; - final actions = _buildActions(context, selection); - final onFilterTap = removableFilters ? collection.removeFilter : null; - return AvesAppBar( - contentHeight: appBarContentHeight, - leading: _buildAppBarLeading( - hasDrawer: appMode.canNavigate, - isSelecting: isSelecting, - ), - title: _buildAppBarTitle(isSelecting), - actions: isTelevision ? [] : actions, - bottom: Column( - children: [ - if (isTelevision) - SizedBox( - height: CaptionedButton.getTelevisionButtonHeight(context), - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 8), - scrollDirection: Axis.horizontal, - children: actions, + return NotificationListener( + // cancel notification bubbling so that the draggable scroll bar + // does not misinterpret filter bar scrolling for collection scrolling + onNotification: (notification) => true, + child: AnimatedBuilder( + animation: collection.filterChangeNotifier, + builder: (context, child) { + final removableFilters = appMode != AppMode.pickMediaInternal; + return Selector( + selector: (context, query) => query.enabled, + builder: (context, queryEnabled, child) { + return Selector>( + selector: (context, s) => s.collectionBrowsingQuickActions, + builder: (context, _, child) { + final useTvLayout = settings.useTvLayout; + final actions = _buildActions(context, selection); + final onFilterTap = removableFilters ? collection.removeFilter : null; + return AvesAppBar( + contentHeight: appBarContentHeight, + leading: _buildAppBarLeading( + hasDrawer: appMode.canNavigate, + isSelecting: isSelecting, + ), + title: _buildAppBarTitle(isSelecting), + actions: useTvLayout ? [] : actions, + bottom: Column( + children: [ + if (useTvLayout) + SizedBox( + height: CaptionedButton.getTelevisionButtonHeight(context), + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + children: actions, + ), ), - ), - if (showFilterBar) - NotificationListener( - onNotification: (notification) { - collection.addFilter(notification.reversedFilter); - return true; - }, - child: FilterBar( - filters: visibleFilters, - onTap: onFilterTap, - onRemove: onFilterTap, + if (showFilterBar) + NotificationListener( + onNotification: (notification) { + collection.addFilter(notification.reversedFilter); + return true; + }, + child: FilterBar( + filters: visibleFilters, + onTap: onFilterTap, + onRemove: onFilterTap, + ), ), - ), - if (queryEnabled) - EntryQueryBar( - queryNotifier: context.select>((query) => query.queryNotifier), - focusNode: _queryBarFocusNode, - ), - ], - ), - transitionKey: isSelecting, - ); - }, - ); - }, - ); - }, + if (queryEnabled) + EntryQueryBar( + queryNotifier: context.select>((query) => query.queryNotifier), + focusNode: _queryBarFocusNode, + ), + ], + ), + transitionKey: isSelecting, + ); + }, + ); + }, + ); + }, + ), ); } double get appBarContentHeight { double height = kToolbarHeight; - if (device.isTelevision) { + if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); } if (showFilterBar) { @@ -227,7 +232,7 @@ class _CollectionAppBarState extends State with SingleTickerPr } Widget? _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) { - if (device.isTelevision) return null; + if (settings.useTvLayout) return null; if (!hasDrawer) { return const CloseButton(); @@ -309,7 +314,7 @@ class _CollectionAppBarState extends State with SingleTickerPr selectedItemCount: selectedItemCount, ); - return device.isTelevision + return settings.useTvLayout ? _buildTelevisionActions( context: context, appMode: appMode, diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 051a3935b..ccf5c138f 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/favourite.dart'; @@ -57,7 +56,7 @@ class CollectionGrid extends StatefulWidget { static const double fixedExtentLayoutSpacing = 2; static const double mosaicLayoutSpacing = 4; - static int get columnCountDefault => device.isTelevision ? 6 : 4; + static int get columnCountDefault => settings.useTvLayout ? 6 : 4; const CollectionGrid({ super.key, @@ -176,7 +175,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { tileLayout: tileLayout, isScrollingNotifier: _isScrollingNotifier, ); - if (!device.isTelevision) return tile; + if (!settings.useTvLayout) return tile; return Focus( onFocusChange: (focused) { diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index ece27a063..501ee402e 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/query.dart'; @@ -122,7 +121,7 @@ class _CollectionPageState extends State { ); Widget page; - if (device.isTelevision) { + if (settings.useTvLayout) { page = Scaffold( body: Row( children: [ diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 93b35158f..865e87f50 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -69,7 +69,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware return isSelecting && selectedItemCount == itemCount; // browsing case EntrySetAction.searchCollection: - return !device.isTelevision && appMode.canNavigate && !isSelecting; + return !settings.useTvLayout && appMode.canNavigate && !isSelecting; case EntrySetAction.toggleTitleSearch: return !isSelecting; case EntrySetAction.addShortcut: @@ -82,7 +82,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.stats: return isMain; case EntrySetAction.rescan: - return !device.isTelevision && isMain && !isTrash; + return !settings.useTvLayout && isMain && !isTrash; // selecting case EntrySetAction.share: case EntrySetAction.toggleFavourite: diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 4856c242e..20f1a47ed 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -82,20 +82,15 @@ class _FilterBarState extends State { // chip border clipping when the floating app bar is fading color: Colors.transparent, height: FilterBar.preferredHeight, - child: NotificationListener( - // cancel notification bubbling so that the draggable scroll bar - // does not misinterpret filter bar scrolling for collection scrolling - onNotification: (notification) => true, - child: AnimatedList( - key: _animatedListKey, - initialItemCount: filters.length, - scrollDirection: Axis.horizontal, - padding: FilterBar.rowPadding, - itemBuilder: (context, index, animation) { - if (index >= filters.length) return const SizedBox(); - return _buildChip(filters.toList()[index]); - }, - ), + child: AnimatedList( + key: _animatedListKey, + initialItemCount: filters.length, + scrollDirection: Axis.horizontal, + padding: FilterBar.rowPadding, + itemBuilder: (context, index, animation) { + if (index >= filters.length) return const SizedBox(); + return _buildChip(filters.toList()[index]); + }, ), ); } diff --git a/lib/widgets/common/basic/color_list_tile.dart b/lib/widgets/common/basic/color_list_tile.dart index babf354e9..bcda737ec 100644 --- a/lib/widgets/common/basic/color_list_tile.dart +++ b/lib/widgets/common/basic/color_list_tile.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; @@ -72,13 +72,13 @@ class _ColorPickerDialogState extends State { @override Widget build(BuildContext context) { - final isTelevision = device.isTelevision; + final useTvLayout = settings.useTvLayout; return AvesDialog( scrollableContent: [ ColorPicker( color: color, onColorChanged: (v) => color = v, - pickersEnabled: isTelevision + pickersEnabled: useTvLayout ? const { ColorPickerType.primary: true, ColorPickerType.accent: false, @@ -90,7 +90,7 @@ class _ColorPickerDialogState extends State { }, hasBorder: true, borderRadius: 20, - subheading: isTelevision ? const SizedBox(height: 16) : null, + subheading: useTvLayout ? const SizedBox(height: 16) : null, ) ], actions: [ diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index d7610b93a..8fc6a44ed 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -127,7 +127,7 @@ class TvTileGridBottomPaddingSliver extends StatelessWidget { Widget build(BuildContext context) { return SliverToBoxAdapter( child: SizedBox( - height: device.isTelevision ? context.select((controller) => controller.spacing) : 0, + height: settings.useTvLayout ? context.select((controller) => controller.spacing) : 0, ), ); } diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart index d996416a4..97480a9af 100644 --- a/lib/widgets/common/behaviour/pop/tv_navigation.dart +++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/settings.dart'; @@ -13,7 +12,7 @@ import 'package:provider/provider.dart'; // address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality class TvNavigationPopHandler { static bool pop(BuildContext context) { - if (!device.isTelevision || _isHome(context)) { + if (!settings.useTvLayout || _isHome(context)) { return true; } diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 341f969e5..887cdc6e1 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -33,7 +33,7 @@ class SectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { Widget child = _buildContent(context); - if (device.isTelevision) { + if (settings.useTvLayout) { final colors = Theme.of(context).colorScheme; child = Material( type: MaterialType.transparency, diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart index 4b556047d..a510aba89 100644 --- a/lib/widgets/common/identity/aves_app_bar.dart +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; @@ -34,7 +33,7 @@ class AvesAppBar extends StatelessWidget { selector: (context, mq) => mq.padding.top, builder: (context, mqPaddingTop, child) { return SliverPersistentHeader( - floating: !device.isTelevision, + floating: !settings.useTvLayout, pinned: false, delegate: _SliverAppBarDelegate( height: mqPaddingTop + appBarHeightForContentHeight(contentHeight), diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index fb1b8b0e7..3ed5f1773 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; @@ -36,7 +35,7 @@ class MapButtonPanel extends StatelessWidget { Widget? navigationButton; switch (context.select((v) => v.navigationButton)) { case MapNavigationButton.back: - if (!device.isTelevision) { + if (!settings.useTvLayout) { navigationButton = MapOverlayButton( icon: const BackButtonIcon(), onPressed: () => Navigator.pop(context), diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart index 54e9d3859..282c38aa1 100644 --- a/lib/widgets/common/search/delegate.dart +++ b/lib/widgets/common/search/delegate.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/route.dart'; @@ -20,7 +20,7 @@ abstract class AvesSearchDelegate extends SearchDelegate { @override Widget? buildLeading(BuildContext context) { - if (device.isTelevision) { + if (settings.useTvLayout) { return const Icon(AIcons.search); } diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index e095ccb7f..cf7335043 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -1,7 +1,6 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/covers.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; @@ -83,7 +82,7 @@ abstract class ChipSetActionDelegate with FeedbackMi return isSelecting && selectedItemCount == itemCount; // browsing case ChipSetAction.search: - return !device.isTelevision && appMode.canNavigate && !isSelecting; + return !settings.useTvLayout && appMode.canNavigate && !isSelecting; case ChipSetAction.toggleTitleSearch: return !isSelecting; case ChipSetAction.createAlbum: diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index e95b110c9..8279e143d 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_set_actions.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/query.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/action_controls/togglers/title_search.dart'; @@ -125,47 +125,52 @@ class _FilterGridAppBarState>>(); final isSelecting = selection.isSelecting; _isSelectingNotifier.value = isSelecting; - return Selector( - selector: (context, query) => query.enabled, - builder: (context, queryEnabled, child) { - ActionsBuilder actionsBuilder = widget.actionsBuilder ?? _buildActions; - final isTelevision = device.isTelevision; - final actions = actionsBuilder(context, appMode, selection, widget.actionDelegate); - return AvesAppBar( - contentHeight: appBarContentHeight, - leading: _buildAppBarLeading( - hasDrawer: appMode.canNavigate, - isSelecting: isSelecting, - ), - title: _buildAppBarTitle(isSelecting), - actions: isTelevision ? [] : actions, - bottom: Column( - children: [ - if (isTelevision) - SizedBox( - height: CaptionedButton.getTelevisionButtonHeight(context), - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 8), - scrollDirection: Axis.horizontal, - children: actions, + return NotificationListener( + // cancel notification bubbling so that the draggable scroll bar + // does not misinterpret filter bar scrolling for collection scrolling + onNotification: (notification) => true, + child: Selector( + selector: (context, query) => query.enabled, + builder: (context, queryEnabled, child) { + ActionsBuilder actionsBuilder = widget.actionsBuilder ?? _buildActions; + final useTvLayout = settings.useTvLayout; + final actions = actionsBuilder(context, appMode, selection, widget.actionDelegate); + return AvesAppBar( + contentHeight: appBarContentHeight, + leading: _buildAppBarLeading( + hasDrawer: appMode.canNavigate, + isSelecting: isSelecting, + ), + title: _buildAppBarTitle(isSelecting), + actions: useTvLayout ? [] : actions, + bottom: Column( + children: [ + if (useTvLayout) + SizedBox( + height: CaptionedButton.getTelevisionButtonHeight(context), + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + children: actions, + ), ), - ), - if (queryEnabled) - FilterQueryBar( - queryNotifier: context.select>((query) => query.queryNotifier), - focusNode: _queryBarFocusNode, - ), - ], - ), - transitionKey: isSelecting, - ); - }, + if (queryEnabled) + FilterQueryBar( + queryNotifier: context.select>((query) => query.queryNotifier), + focusNode: _queryBarFocusNode, + ), + ], + ), + transitionKey: isSelecting, + ); + }, + ), ); } double get appBarContentHeight { double height = kToolbarHeight; - if (device.isTelevision) { + if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); } if (context.read().enabled) { @@ -175,7 +180,7 @@ class _FilterGridAppBarState extends StatelessWidget { ), ); - if (device.isTelevision) { + if (settings.useTvLayout) { return Scaffold( body: Row( children: [ @@ -202,7 +201,7 @@ class _FilterGridState extends State<_FilterGrid> Widget build(BuildContext context) { _tileExtentController ??= TileExtentController( settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, - columnCountDefault: device.isTelevision ? 4 : 3, + columnCountDefault: settings.useTvLayout ? 4 : 3, extentMin: 60, extentMax: 300, spacing: 8, @@ -356,7 +355,7 @@ class _FilterGridContentState extends State<_FilterG banner: _getFilterBanner(context, gridItem.filter), heroType: widget.heroType, ); - if (!device.isTelevision) return tile; + if (!settings.useTvLayout) return tile; return Focus( onFocusChange: (focused) { diff --git a/lib/widgets/navigation/tv_rail.dart b/lib/widgets/navigation/tv_rail.dart index b7eb63e2e..9bde428ce 100644 --- a/lib/widgets/navigation/tv_rail.dart +++ b/lib/widgets/navigation/tv_rail.dart @@ -109,25 +109,28 @@ class _TvRailState extends State { ), ); - return Column( - children: [ - const SizedBox(height: 8), - header, - const SizedBox(height: 4), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - controller: _scrollController, - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight(child: rail), - ), - ); - }, + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + header, + const SizedBox(height: 4), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + controller: _scrollController, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight(child: rail), + ), + ); + }, + ), ), - ), - ], + ], + ), ); } diff --git a/lib/widgets/settings/accessibility/accessibility.dart b/lib/widgets/settings/accessibility/accessibility.dart index 6f90eb815..484414f3a 100644 --- a/lib/widgets/settings/accessibility/accessibility.dart +++ b/lib/widgets/settings/accessibility/accessibility.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; @@ -29,7 +28,7 @@ class AccessibilitySection extends SettingsSection { @override FutureOr> tiles(BuildContext context) => [ - if (!device.isTelevision) SettingsTileAccessibilityShowPinchGestureAlternatives(), + if (!settings.useTvLayout) SettingsTileAccessibilityShowPinchGestureAlternatives(), SettingsTileAccessibilityAnimations(), SettingsTileAccessibilityTimeToTakeAction(), ]; diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index 48ec43eb3..77e81f6ed 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -8,6 +8,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; @@ -29,11 +30,12 @@ class DisplaySection extends SettingsSection { @override FutureOr> tiles(BuildContext context) => [ - if (!device.isTelevision) SettingsTileDisplayThemeBrightness(), + if (!settings.useTvLayout) SettingsTileDisplayThemeBrightness(), SettingsTileDisplayThemeColorMode(), if (device.isDynamicColorAvailable) SettingsTileDisplayEnableDynamicColor(), SettingsTileDisplayEnableBlurEffect(), - if (!device.isTelevision) SettingsTileDisplayDisplayRefreshRateMode(), + if (!settings.useTvLayout) SettingsTileDisplayRefreshRateMode(), + if (!device.isTelevision) SettingsTileDisplayForceTvLayout(), ]; } @@ -88,7 +90,7 @@ class SettingsTileDisplayEnableBlurEffect extends SettingsTile { ); } -class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { +class SettingsTileDisplayRefreshRateMode extends SettingsTile { @override String title(BuildContext context) => context.l10n.settingsDisplayRefreshRateModeTile; @@ -102,3 +104,41 @@ class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { dialogTitle: context.l10n.settingsDisplayRefreshRateModeDialogTitle, ); } + +class SettingsTileDisplayForceTvLayout extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsDisplayUseTvInterface; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.forceTvLayout, + onChanged: (v) async { + final confirmed = await showDialog( + context: context, + builder: (context) { + final l10n = context.l10n; + return AvesDialog( + content: Text([ + l10n.settingsModificationWarningDialogMessage, + l10n.genericDangerWarningDialogMessage, + ].join('\n\n')), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(l10n.applyButtonLabel), + ), + ], + ); + }, + ); + if (confirmed == null || !confirmed) return; + + settings.forceTvLayout = v; + }, + title: title(context), + ); +} diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 4b756171f..2c738b136 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/enums/screen_on.dart'; @@ -32,11 +31,11 @@ class NavigationSection extends SettingsSection { @override FutureOr> tiles(BuildContext context) => [ SettingsTileNavigationHomePage(), - if (!device.isTelevision) SettingsTileNavigationKeepScreenOn(), - if (!device.isTelevision) SettingsTileShowBottomNavigationBar(), - if (!device.isTelevision) SettingsTileNavigationDoubleBackExit(), + if (!settings.useTvLayout) SettingsTileNavigationKeepScreenOn(), + if (!settings.useTvLayout) SettingsTileShowBottomNavigationBar(), + if (!settings.useTvLayout) SettingsTileNavigationDoubleBackExit(), SettingsTileNavigationDrawer(), - if (!device.isTelevision) SettingsTileNavigationConfirmationDialog(), + if (!settings.useTvLayout) SettingsTileNavigationConfirmationDialog(), ]; } diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index 4cbe168fe..31f002afe 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -34,11 +34,11 @@ class PrivacySection extends SettingsSection { return [ SettingsTilePrivacyAllowInstalledAppAccess(), if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(), - if (!device.isTelevision && device.canRequestManageMedia) SettingsTilePrivacyManageMedia(), + if (!settings.useTvLayout && device.canRequestManageMedia) SettingsTilePrivacyManageMedia(), SettingsTilePrivacySaveSearchHistory(), - if (!device.isTelevision) SettingsTilePrivacyEnableBin(), + if (!settings.useTvLayout) SettingsTilePrivacyEnableBin(), SettingsTilePrivacyHiddenItems(), - if (!device.isTelevision && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(), + if (!settings.useTvLayout && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(), ]; } } diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index cf8a6c445..72d0f7acb 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:aves/model/actions/settings_actions.dart'; -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; @@ -73,7 +73,7 @@ class _SettingsPageState extends State with FeedbackMixin { Widget build(BuildContext context) { final appBarTitle = Text(context.l10n.settingsPageTitle); - if (device.isTelevision) { + if (settings.useTvLayout) { return Scaffold( body: AvesPopScope( handlers: const [TvNavigationPopHandler.pop], diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index 351a78052..2bf00f4e2 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -25,7 +25,7 @@ class ThumbnailsSection extends SettingsSection { @override List tiles(BuildContext context) => [ - if (!device.isTelevision) SettingsTileCollectionQuickActions(), + if (!settings.useTvLayout) SettingsTileCollectionQuickActions(), SettingsTileThumbnailOverlay(), ]; } diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index cc6387cf1..d7c3b978e 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/video_auto_play_mode.dart'; @@ -43,7 +42,7 @@ class VideoSection extends SettingsSection { SettingsTileVideoEnableHardwareAcceleration(), SettingsTileVideoEnableAutoPlay(), SettingsTileVideoLoopMode(), - if (!device.isTelevision) SettingsTileVideoControls(), + if (!settings.useTvLayout) SettingsTileVideoControls(), SettingsTileVideoSubtitleTheme(), ]; } diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index cf76ca7f1..94f76ae3a 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; @@ -20,7 +19,7 @@ class ViewerOverlayPage extends StatelessWidget { body: SafeArea( child: ListView( children: [ - if (!device.isTelevision) + if (!settings.useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayOnOpening, onChanged: (v) => settings.showOverlayOnOpening = v, @@ -68,13 +67,13 @@ class ViewerOverlayPage extends StatelessWidget { ); }, ), - if (!device.isTelevision) + if (!settings.useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayMinimap, onChanged: (v) => settings.showOverlayMinimap = v, title: context.l10n.settingsViewerShowMinimap, ), - if (!device.isTelevision) + if (!settings.useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayThumbnailPreview, onChanged: (v) => settings.showOverlayThumbnailPreview = v, diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 557747f49..1f9707568 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; @@ -34,12 +33,12 @@ class ViewerSection extends SettingsSection { FutureOr> tiles(BuildContext context) async { final isCutoutAware = await windowService.isCutoutAware(); return [ - if (!device.isTelevision) SettingsTileViewerQuickActions(), + if (!settings.useTvLayout) SettingsTileViewerQuickActions(), SettingsTileViewerOverlay(), SettingsTileViewerSlideshow(), - if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(), - if (!device.isTelevision && isCutoutAware) SettingsTileViewerUseCutout(), - if (!device.isTelevision) SettingsTileViewerMaxBrightness(), + if (!settings.useTvLayout) SettingsTileViewerGestureSideTapNext(), + if (!settings.useTvLayout && isCutoutAware) SettingsTileViewerUseCutout(), + if (!settings.useTvLayout) SettingsTileViewerMaxBrightness(), SettingsTileViewerMotionPhotoAutoPlay(), SettingsTileViewerImageBackground(), ]; diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index f22187cee..b240f6273 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; @@ -226,7 +225,7 @@ class _StatsPageState extends State { return Scaffold( appBar: AppBar( - automaticallyImplyLeading: !device.isTelevision, + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.statsPageTitle), ), body: GestureAreaProtectorStack( @@ -356,7 +355,7 @@ class StatsTopPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - automaticallyImplyLeading: !device.isTelevision, + automaticallyImplyLeading: !settings.useTvLayout, title: Text(title), ), body: GestureAreaProtectorStack( diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 9f05f2790..fa7048fcb 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -93,7 +93,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.videoCaptureFrame: return canWrite && targetEntry.isVideo; case EntryAction.videoToggleMute: - return !device.isTelevision && targetEntry.isVideo; + return !settings.useTvLayout && targetEntry.isVideo; case EntryAction.videoSelectStreams: case EntryAction.videoSetSpeed: case EntryAction.videoSettings: @@ -103,13 +103,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.openVideo: return targetEntry.isVideo; case EntryAction.rotateScreen: - return !device.isTelevision && settings.isRotationLocked; + return !settings.useTvLayout && settings.isRotationLocked; case EntryAction.addShortcut: return device.canPinShortcut; case EntryAction.edit: return canWrite; case EntryAction.copyToClipboard: - return !device.isTelevision; + return !settings.useTvLayout; case EntryAction.info: case EntryAction.open: case EntryAction.setAs: diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 206d3750b..76c11f35a 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -4,7 +4,6 @@ import 'dart:ui'; import 'package:aves/app_mode.dart'; 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/model/source/collection_lens.dart'; @@ -181,12 +180,12 @@ class _ViewerVerticalPageViewState extends State { } Widget _buildImagePage() { - final isTelevision = device.isTelevision; + final useTvLayout = settings.useTvLayout; Widget? child; Map? shortcuts = { - const SingleActivator(LogicalKeyboardKey.arrowUp): isTelevision ? const TvShowLessInfoIntent() : const _LeaveIntent(), - const SingleActivator(LogicalKeyboardKey.arrowDown): isTelevision ? const _TvShowMoreInfoIntent() : const _ShowInfoIntent(), + const SingleActivator(LogicalKeyboardKey.arrowUp): useTvLayout ? const TvShowLessInfoIntent() : const _LeaveIntent(), + const SingleActivator(LogicalKeyboardKey.arrowDown): useTvLayout ? const _TvShowMoreInfoIntent() : const _ShowInfoIntent(), const SingleActivator(LogicalKeyboardKey.mediaPause): const _PlayPauseIntent.pause(), const SingleActivator(LogicalKeyboardKey.mediaPlay): const _PlayPauseIntent.play(), const SingleActivator(LogicalKeyboardKey.mediaPlayPause): const _PlayPauseIntent.toggle(), @@ -211,7 +210,7 @@ class _ViewerVerticalPageViewState extends State { ); } if (child != null) { - if (device.isTelevision) { + if (settings.useTvLayout) { child = ValueListenableBuilder( valueListenable: _isImageFocusedNotifier, builder: (context, isImageFocused, child) { @@ -238,7 +237,7 @@ class _ViewerVerticalPageViewState extends State { _TvShowMoreInfoIntent: CallbackAction(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)), _PlayPauseIntent: CallbackAction<_PlayPauseIntent>(onInvoke: (intent) => _onPlayPauseIntent(intent, entry)), ActivateIntent: CallbackAction(onInvoke: (intent) { - if (isTelevision) { + if (useTvLayout) { final _entry = entry; if (_entry != null && _entry.isVideo) { // address `TV-PC` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 07d077396..2484943b5 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -689,7 +689,9 @@ class _EntryViewerStackState extends State with EntryViewContr await AvesApp.showSystemUI(); AvesApp.setSystemUIStyle(context); - await windowService.requestOrientation(); + if (!settings.useTvLayout) { + await windowService.requestOrientation(); + } } // overlay diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index ac1be650e..71115e927 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,6 +1,6 @@ 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/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -37,7 +37,7 @@ class InfoAppBar extends StatelessWidget { final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); return SliverAppBar( - leading: device.isTelevision + leading: settings.useTvLayout ? null : IconButton( // key is expected by test driver diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 47f68f93a..94460ccca 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -186,7 +185,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); final viewerButtonRow = FocusableActionDetector( focusNode: _buttonRowFocusScopeNode, - shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, + shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, child: SafeArea( top: false, diff --git a/lib/widgets/viewer/overlay/slideshow_buttons.dart b/lib/widgets/viewer/overlay/slideshow_buttons.dart index 645c6a6e5..1266594e8 100644 --- a/lib/widgets/viewer/overlay/slideshow_buttons.dart +++ b/lib/widgets/viewer/overlay/slideshow_buttons.dart @@ -1,5 +1,5 @@ import 'package:aves/model/actions/slideshow_actions.dart'; -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; @@ -70,9 +70,9 @@ class _SlideshowButtonsState extends State { Widget build(BuildContext context) { return FocusableActionDetector( focusNode: _buttonRowFocusScopeNode, - shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, + shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, - child: device.isTelevision + child: settings.useTvLayout ? Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 9d6bd33ee..24235bfcb 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -1,5 +1,4 @@ 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/model/source/collection_lens.dart'; @@ -51,7 +50,7 @@ class ViewerButtons extends StatelessWidget { Widget build(BuildContext context) { final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection); - if (device.isTelevision) { + if (settings.useTvLayout) { return _TvButtonRowContent( actionDelegate: actionDelegate, scale: scale, diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index a47584a8b..69a38bfd5 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,9 +1,9 @@ import 'dart:math'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/model/panorama.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/basic/insets.dart'; @@ -110,7 +110,7 @@ class _PanoramaPageState extends State { } Widget _buildOverlay(BuildContext context) { - if (device.isTelevision) return const SizedBox(); + if (settings.useTvLayout) return const SizedBox(); return TooltipTheme( data: TooltipTheme.of(context).copyWith( diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index c9e4cf468..8b867ab68 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -1,5 +1,4 @@ import 'package:aves/app_flavor.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; @@ -71,7 +70,7 @@ class _WelcomePageState extends State { child: child, ), ), - children: device.isTelevision + children: settings.useTvLayout ? [ ..._buildHeader(context, isPortrait: isPortrait), Padding( diff --git a/untranslated.json b/untranslated.json index a21f5bec5..d90aebfda 100644 --- a/untranslated.json +++ b/untranslated.json @@ -387,6 +387,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -526,6 +527,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -592,21 +594,29 @@ ], "cs": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "de": [ "columnCount", + "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", - "settingsAccessibilityShowPinchGestureAlternatives" + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ], "el": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "es": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "fa": [ @@ -867,6 +877,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionImport", @@ -1002,6 +1013,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -1074,6 +1086,11 @@ "filePickerUseThisFolder" ], + "fr": [ + "settingsModificationWarningDialogMessage", + "settingsDisplayUseTvInterface" + ], + "gl": [ "columnCount", "entryActionShareImageOnly", @@ -1329,6 +1346,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -1468,6 +1486,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -1543,11 +1562,15 @@ ], "id": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "it": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "ja": [ @@ -1559,16 +1582,25 @@ "keepScreenOnVideoPlayback", "subtitlePositionTop", "subtitlePositionBottom", + "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" ], + "ko": [ + "settingsModificationWarningDialogMessage", + "settingsDisplayUseTvInterface" + ], + "lt": [ "columnCount", "keepScreenOnVideoPlayback", + "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", - "settingsAccessibilityShowPinchGestureAlternatives" + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ], "nb": [ @@ -1577,8 +1609,10 @@ "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", "keepScreenOnVideoPlayback", + "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", - "settingsAccessibilityShowPinchGestureAlternatives" + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ], "nl": [ @@ -1595,11 +1629,13 @@ "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "settingsModificationWarningDialogMessage", "settingsViewerShowRatingTags", "settingsViewerShowDescription", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" ], @@ -1841,6 +1877,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -1980,6 +2017,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -2349,6 +2387,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -2488,6 +2527,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -2576,16 +2616,25 @@ "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "settingsModificationWarningDialogMessage", "settingsViewerShowRatingTags", "settingsViewerShowDescription", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" ], "ro": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" + ], + + "ru": [ + "settingsModificationWarningDialogMessage", + "settingsDisplayUseTvInterface" ], "th": [ @@ -2718,6 +2767,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -2857,6 +2907,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -2932,21 +2983,29 @@ ], "tr": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "uk": [ - "settingsViewerShowDescription" + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "zh": [ + "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", - "settingsAccessibilityShowPinchGestureAlternatives" + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ], "zh_Hant": [ "columnCount", + "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", - "settingsAccessibilityShowPinchGestureAlternatives" + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ] }