diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index ae2871825..4d2303594 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -47,7 +47,6 @@ class AboutPage extends StatelessWidget { ); if (settings.useTvLayout) { - final isRtl = context.isRtl; return Scaffold( body: AvesPopScope( handlers: const [TvNavigationPopHandler.pop], @@ -57,9 +56,8 @@ class AboutPage extends StatelessWidget { controller: context.read(), ), Expanded( - child: SafeArea( - left: isRtl, - right: !isRtl, + child: DirectionalSafeArea( + start: false, child: body, ), ), diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index d5c139982..4d6b723b6 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -136,11 +136,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { final Set _changedUris = {}; Size? _screenSize; - // Flutter has various page transition implementations for Android: - // - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below - // - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 - // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0) - final ValueNotifier _pageTransitionsBuilderNotifier = ValueNotifier(const FadeUpwardsPageTransitionsBuilder()); + final ValueNotifier _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder); final ValueNotifier _tvMediaQueryModifierNotifier = ValueNotifier(null); final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.main); @@ -152,6 +148,12 @@ class _AvesAppState extends State with WidgetsBindingObserver { final EventChannel _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events'); final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error'); + // Flutter has various page transition implementations for Android: + // - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below + // - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 + // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0) + static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder(); + @override void initState() { super.initState(); @@ -420,7 +422,19 @@ class _AvesAppState extends State with WidgetsBindingObserver { await settings.init(monitorPlatformSettings: true); settings.isRotationLocked = await windowService.isRotationLocked(); settings.areAnimationsRemoved = await AccessibilityService.areAnimationsRemoved(); + await _onTvLayoutChanged(); + _monitorSettings(); + + FijkLog.setLevel(FijkLogLevel.Warn); + unawaited(_setupErrorReporting()); + + debugPrint('App setup in ${stopwatch.elapsed.inMilliseconds}ms'); + } + + Future _onTvLayoutChanged() async { if (settings.useTvLayout) { + settings.applyTvSettings(); + _pageTransitionsBuilderNotifier.value = const TvPageTransitionsBuilder(); _tvMediaQueryModifierNotifier.value = (mq) => mq.copyWith( textScaleFactor: 1.1, @@ -429,13 +443,11 @@ class _AvesAppState extends State with WidgetsBindingObserver { if (settings.forceTvLayout) { await windowService.requestOrientation(Orientation.landscape); } + } else { + _pageTransitionsBuilderNotifier.value = defaultPageTransitionsBuilder; + _tvMediaQueryModifierNotifier.value = null; + await windowService.requestOrientation(null); } - _monitorSettings(); - - FijkLog.setLevel(FijkLogLevel.Warn); - unawaited(_setupErrorReporting()); - - debugPrint('App setup in ${stopwatch.elapsed.inMilliseconds}ms'); } void _monitorSettings() { @@ -457,22 +469,26 @@ class _AvesAppState extends State with WidgetsBindingObserver { } void applyForceTvLayout() { - settings.applyTvSettings(); - windowService.requestOrientation(settings.forceTvLayout ? Orientation.landscape : null); - AvesApp.navigatorKey.currentState!.pushAndRemoveUntil( + _onTvLayoutChanged(); + unawaited(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()); + final settingStream = settings.updateStream; + // app + settingStream.where((event) => event.key == Settings.isInstalledAppAccessAllowedKey).listen((_) => applyIsInstalledAppAccessAllowed()); + // display + settingStream.where((event) => event.key == Settings.displayRefreshRateModeKey).listen((_) => applyDisplayRefreshRateMode()); + settingStream.where((event) => event.key == Settings.forceTvLayoutKey).listen((_) => applyForceTvLayout()); + // navigation + settingStream.where((event) => event.key == Settings.keepScreenOnKey).listen((_) => applyKeepScreenOn()); + // platform settings + settingStream.where((event) => event.key == Settings.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked()); applyDisplayRefreshRateMode(); applyKeepScreenOn(); diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 501ee402e..39b2ea928 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -82,6 +82,7 @@ class _CollectionPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return SelectionProvider( child: Selector, bool>( @@ -104,11 +105,12 @@ class _CollectionPageState extends State { TvNavigationPopHandler.pop, _doubleBackPopHandler.pop, ], - child: const GestureAreaProtectorStack( - child: SafeArea( + child: GestureAreaProtectorStack( + child: DirectionalSafeArea( + start: !useTvLayout, top: false, bottom: false, - child: CollectionGrid( + child: const CollectionGrid( // key is expected by test driver key: Key('collection-grid'), settingsRouteKey: CollectionPage.routeName, @@ -121,7 +123,7 @@ class _CollectionPageState extends State { ); Widget page; - if (settings.useTvLayout) { + if (useTvLayout) { page = Scaffold( body: Row( children: [ diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 8fc6a44ed..fa1a73d46 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; @@ -188,3 +189,35 @@ extension ExtraMediaQueryData on MediaQueryData { ); } } + +class DirectionalSafeArea extends StatelessWidget { + final bool start, top, end, bottom; + final EdgeInsets minimum; + final bool maintainBottomViewPadding; + final Widget child; + + const DirectionalSafeArea({ + super.key, + this.start = true, + this.top = true, + this.end = true, + this.bottom = true, + this.minimum = EdgeInsets.zero, + this.maintainBottomViewPadding = false, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final isRtl = context.isRtl; + return SafeArea( + left: isRtl ? end : start, + top: top, + right: isRtl ? start : end, + bottom: bottom, + minimum: minimum, + maintainBottomViewPadding: maintainBottomViewPadding, + child: child, + ); + } +} diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart index a510aba89..20d81b68f 100644 --- a/lib/widgets/common/identity/aves_app_bar.dart +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -2,6 +2,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -32,12 +33,14 @@ class AvesAppBar extends StatelessWidget { return Selector( selector: (context, mq) => mq.padding.top, builder: (context, mqPaddingTop, child) { + final useTvLayout = settings.useTvLayout; return SliverPersistentHeader( - floating: !settings.useTvLayout, + floating: !useTvLayout, pinned: false, delegate: _SliverAppBarDelegate( height: mqPaddingTop + appBarHeightForContentHeight(contentHeight), - child: SafeArea( + child: DirectionalSafeArea( + start: !useTvLayout, bottom: false, child: AvesFloatingBar( builder: (context, backgroundColor, child) => Material( diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 37644b5b2..beb3114f3 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -77,10 +77,12 @@ class FilterGridPage extends StatelessWidget { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; final body = QueryProvider( initialQuery: null, child: GestureAreaProtectorStack( - child: SafeArea( + child: DirectionalSafeArea( + start: !useTvLayout, top: false, bottom: false, child: Selector( @@ -112,7 +114,7 @@ class FilterGridPage extends StatelessWidget { ), ); - if (settings.useTvLayout) { + if (useTvLayout) { return Scaffold( body: Row( children: [ diff --git a/lib/widgets/navigation/tv_rail.dart b/lib/widgets/navigation/tv_rail.dart index 9bde428ce..fb1eb8bde 100644 --- a/lib/widgets/navigation/tv_rail.dart +++ b/lib/widgets/navigation/tv_rail.dart @@ -9,6 +9,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; @@ -32,6 +33,8 @@ class TvRail extends StatefulWidget { final CollectionLens? currentCollection; final TvRailController controller; + static const double minExtendedWidth = 256; + const TvRail({ super.key, required this.controller, @@ -44,6 +47,7 @@ class TvRail extends StatefulWidget { class _TvRailState extends State { late final ScrollController _scrollController; + final ValueNotifier _extendedNotifier = ValueNotifier(true); final FocusNode _focusNode = FocusNode(); TvRailController get controller => widget.controller; @@ -64,72 +68,82 @@ class _TvRailState extends State { void dispose() { _scrollController.removeListener(_onScrollChanged); _scrollController.dispose(); + _extendedNotifier.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final header = Row( - children: [ - const AvesLogo(size: 48), - const SizedBox(width: 16), - Text( - context.l10n.appName, - style: const TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.w300, - letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], - ), - ), - ], - ); - final navEntries = _getNavEntries(context); + return DirectionalSafeArea( + end: false, + child: ValueListenableBuilder( + valueListenable: _extendedNotifier, + builder: (context, extended, child) { + const logo = AvesLogo(size: 48); + final header = extended + ? Row( + children: [ + logo, + const SizedBox(width: 16), + Text( + context.l10n.appName, + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.w300, + letterSpacing: 1.0, + fontFeatures: [FontFeature.enable('smcp')], + ), + ), + ], + ) + : logo; - final rail = Focus( - focusNode: _focusNode, - skipTraversal: true, - child: NavigationRail( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - extended: true, - destinations: navEntries - .map((v) => NavigationRailDestination( - icon: v.icon, - label: v.label, - )) - .toList(), - selectedIndex: max(0, navEntries.indexWhere(((v) => v.isSelected))), - onDestinationSelected: (index) { - controller.focusedIndex = index; - navEntries[index].onSelection(); - }, - ), - ); - - 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), - ), - ); + final rail = Focus( + focusNode: _focusNode, + skipTraversal: true, + child: NavigationRail( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + extended: extended, + destinations: navEntries + .map((v) => NavigationRailDestination( + icon: v.icon, + label: v.label, + )) + .toList(), + selectedIndex: max(0, navEntries.indexWhere(((v) => v.isSelected))), + onDestinationSelected: (index) { + controller.focusedIndex = index; + navEntries[index].onSelection(); }, + minExtendedWidth: TvRail.minExtendedWidth, ), - ), - ], + ); + + return 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/display/display.dart b/lib/widgets/settings/display/display.dart index 77e81f6ed..bda058a5f 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -113,29 +113,31 @@ class SettingsTileDisplayForceTvLayout extends SettingsTile { 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; + if (v) { + 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; }, diff --git a/lib/widgets/settings/language/locale_selection_page.dart b/lib/widgets/settings/language/locale_selection_page.dart index b799bea65..7a8621a7b 100644 --- a/lib/widgets/settings/language/locale_selection_page.dart +++ b/lib/widgets/settings/language/locale_selection_page.dart @@ -30,8 +30,10 @@ class _LocaleSelectionPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.settingsLanguagePageTitle), ), body: SafeArea( @@ -41,10 +43,11 @@ class _LocaleSelectionPageState extends State { final upQuery = query.toUpperCase().trim(); return ListView( children: [ - QueryBar( - queryNotifier: _queryNotifier, - leadingPadding: const EdgeInsetsDirectional.only(start: 24, end: 8), - ), + if (!useTvLayout) + QueryBar( + queryNotifier: _queryNotifier, + leadingPadding: const EdgeInsetsDirectional.only(start: 24, end: 8), + ), ..._getLocaleOptions(context).entries.where((kv) { if (upQuery.isEmpty) return true; final title = kv.value; diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index e0a65ef2d..e0df890b5 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -93,6 +93,7 @@ class _NavigationDrawerEditorPageState extends State length: tabs.length, child: Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsNavigationDrawerEditorPageTitle), bottom: TabBar( tabs: tabs.map((t) => t.item1).toList(), diff --git a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart index c8dc37ccb..c0786f6e3 100644 --- a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart +++ b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart @@ -150,11 +150,13 @@ class _FilePickerPageState extends State { return Drawer( child: ListView( children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - context.l10n.filePickerOpenFrom, - style: Theme.of(context).textTheme.headlineSmall, + SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + context.l10n.filePickerOpenFrom, + style: Theme.of(context).textTheme.headlineSmall, + ), ), ), ...volumes.map((v) { diff --git a/lib/widgets/settings/privacy/hidden_items_page.dart b/lib/widgets/settings/privacy/hidden_items_page.dart index f280d48c3..9a58f892e 100644 --- a/lib/widgets/settings/privacy/hidden_items_page.dart +++ b/lib/widgets/settings/privacy/hidden_items_page.dart @@ -36,6 +36,7 @@ class HiddenItemsPage extends StatelessWidget { length: tabs.length, child: Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsHiddenItemsPageTitle), bottom: TabBar( tabs: tabs.map((t) => t.item1).toList(), diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 72d0f7acb..b32bfc941 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -106,6 +106,7 @@ class _SettingsPageState extends State with FeedbackMixin { .toList(), selectedIndex: selectedIndex, onDestinationSelected: (index) => _tvSelectedIndexNotifier.value = index, + minExtendedWidth: TvRail.minExtendedWidth, ); return LayoutBuilder( builder: (context, constraints) { @@ -118,8 +119,13 @@ class _SettingsPageState extends State with FeedbackMixin { ), ), Expanded( - child: _SettingsSectionBody( - loader: Future.value(sections[selectedIndex].tiles(context)), + child: MediaQuery.removePadding( + context: context, + removeLeft: !context.isRtl, + removeRight: context.isRtl, + child: _SettingsSectionBody( + loader: Future.value(sections[selectedIndex].tiles(context)), + ), ), ), ], diff --git a/lib/widgets/settings/thumbnails/overlay.dart b/lib/widgets/settings/thumbnails/overlay.dart index 928a72638..f386bf2f2 100644 --- a/lib/widgets/settings/thumbnails/overlay.dart +++ b/lib/widgets/settings/thumbnails/overlay.dart @@ -19,6 +19,7 @@ class ThumbnailOverlayPage extends StatelessWidget { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsThumbnailOverlayPageTitle), ), body: SafeArea( diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index 66c597032..14fb00feb 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -18,6 +18,7 @@ class SubtitleThemePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsSubtitleThemePageTitle), ), body: SafeArea( diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index 94f76ae3a..acfd88678 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -12,14 +12,16 @@ class ViewerOverlayPage extends StatelessWidget { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.settingsViewerOverlayPageTitle), ), body: SafeArea( child: ListView( children: [ - if (!settings.useTvLayout) + if (!useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayOnOpening, onChanged: (v) => settings.showOverlayOnOpening = v, @@ -67,13 +69,13 @@ class ViewerOverlayPage extends StatelessWidget { ); }, ), - if (!settings.useTvLayout) + if (!useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayMinimap, onChanged: (v) => settings.showOverlayMinimap = v, title: context.l10n.settingsViewerShowMinimap, ), - if (!settings.useTvLayout) + if (!useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayThumbnailPreview, onChanged: (v) => settings.showOverlayThumbnailPreview = v, diff --git a/lib/widgets/settings/viewer/slideshow.dart b/lib/widgets/settings/viewer/slideshow.dart index 4849bf88e..208bca58d 100644 --- a/lib/widgets/settings/viewer/slideshow.dart +++ b/lib/widgets/settings/viewer/slideshow.dart @@ -16,6 +16,7 @@ class ViewerSlideshowPage extends StatelessWidget { final l10n = context.l10n; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsViewerSlideshowPageTitle), ), body: SafeArea(