From 21938ab1b17ad3e357c58e9ea941d15c82596014 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Apr 2022 11:14:46 +0900 Subject: [PATCH] settings search --- lib/l10n/app_en.arb | 4 + .../settings/accessibility/accessibility.dart | 68 ++++--- lib/widgets/settings/common/tiles.dart | 28 +++ lib/widgets/settings/display/display.dart | 100 ++++++---- lib/widgets/settings/language/language.dart | 100 +++++----- .../navigation/confirmation_dialogs.dart | 20 -- lib/widgets/settings/navigation/drawer.dart | 20 -- .../settings/navigation/navigation.dart | 128 ++++++++----- .../settings/privacy/access_grants.dart | 20 -- .../settings/privacy/hidden_items.dart | 20 -- lib/widgets/settings/privacy/privacy.dart | 162 ++++++++++------ lib/widgets/settings/settings_definition.dart | 43 +++++ lib/widgets/settings/settings_page.dart | 46 +++-- lib/widgets/settings/settings_search.dart | 114 ++++++++++++ .../thumbnails/collection_actions_editor.dart | 20 -- lib/widgets/settings/thumbnails/overlay.dart | 93 ++++++++++ .../settings/thumbnails/thumbnails.dart | 125 ++++--------- lib/widgets/settings/video/controls.dart | 20 -- .../settings/video/subtitle_theme.dart | 20 -- lib/widgets/settings/video/video.dart | 161 +++++++++------- .../settings/video/video_settings_page.dart | 51 ++++++ lib/widgets/settings/viewer/overlay.dart | 20 -- lib/widgets/settings/viewer/viewer.dart | 173 ++++++++++-------- .../viewer/viewer_actions_editor.dart | 20 -- lib/widgets/viewer/video_action_delegate.dart | 2 +- untranslated.json | 68 ++++++- 26 files changed, 1014 insertions(+), 632 deletions(-) create mode 100644 lib/widgets/settings/settings_definition.dart create mode 100644 lib/widgets/settings/settings_search.dart create mode 100644 lib/widgets/settings/thumbnails/overlay.dart create mode 100644 lib/widgets/settings/video/video_settings_page.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index be9d5a62a..6ecea6961 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -588,6 +588,8 @@ "settingsSystemDefault": "System", "settingsDefault": "Default", + "settingsSearchFieldLabel": "Search settings", + "settingsSearchEmpty": "No matching setting", "settingsActionExport": "Export", "settingsActionImport": "Import", @@ -616,6 +618,8 @@ "settingsNavigationDrawerAddAlbum": "Add album", "settingsSectionThumbnails": "Thumbnails", + "settingsThumbnailOverlayTile": "Overlay", + "settingsThumbnailOverlayTitle": "Overlay", "settingsThumbnailShowFavouriteIcon": "Show favorite icon", "settingsThumbnailShowLocationIcon": "Show location icon", "settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon", diff --git a/lib/widgets/settings/accessibility/accessibility.dart b/lib/widgets/settings/accessibility/accessibility.dart index 5d44b6853..418b163c6 100644 --- a/lib/widgets/settings/accessibility/accessibility.dart +++ b/lib/widgets/settings/accessibility/accessibility.dart @@ -1,45 +1,57 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/enums.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'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/accessibility/time_to_take_action.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'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class AccessibilitySection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const AccessibilitySection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class AccessibilitySection extends SettingsSection { + @override + String get key => 'accessibility'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.accessibility, color: context.select((v) => v.accessibility), - ), - title: context.l10n.settingsSectionAccessibility, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSelectionListTile( - values: AccessibilityAnimations.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.accessibilityAnimations, - onSelection: (v) => settings.accessibilityAnimations = v, - tileTitle: context.l10n.settingsRemoveAnimationsTile, - dialogTitle: context.l10n.settingsRemoveAnimationsTitle, - ), - const TimeToTakeActionTile(), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionAccessibility; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileAccessibilityAnimations(), + SettingsTileAccessibilityTimeToTakeAction(), + ]; +} + +class SettingsTileAccessibilityAnimations extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsRemoveAnimationsTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: AccessibilityAnimations.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.accessibilityAnimations, + onSelection: (v) => settings.accessibilityAnimations = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsRemoveAnimationsTitle, + ); +} + +class SettingsTileAccessibilityTimeToTakeAction extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsTimeToTakeActionTile; + + @override + Widget build(BuildContext context) => const TimeToTakeActionTile(); } diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart index eee450281..14c4a3798 100644 --- a/lib/widgets/settings/common/tiles.dart +++ b/lib/widgets/settings/common/tiles.dart @@ -4,6 +4,34 @@ import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +class SettingsSubPageTile extends StatelessWidget { + final String title, routeName; + final WidgetBuilder builder; + + const SettingsSubPageTile({ + Key? key, + required this.title, + required this.routeName, + required this.builder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: builder, + ), + ); + }, + ); + } +} + class SettingsSwitchListTile extends StatelessWidget { final bool Function(BuildContext, Settings) selector; final ValueChanged onChanged; diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index 76f153a28..d83069dc3 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/theme_brightness.dart'; @@ -5,53 +7,71 @@ 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/common/identity/aves_expansion_tile.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'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class DisplaySection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const DisplaySection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class DisplaySection extends SettingsSection { + @override + String get key => 'display'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.display, color: context.select((v) => v.display), - ), - title: context.l10n.settingsSectionDisplay, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSelectionListTile( - values: AvesThemeBrightness.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.themeBrightness, - onSelection: (v) => settings.themeBrightness = v, - tileTitle: context.l10n.settingsThemeBrightness, - dialogTitle: context.l10n.settingsThemeBrightness, - ), - SettingsSwitchListTile( - selector: (context, s) => s.themeColorMode == AvesThemeColorMode.polychrome, - onChanged: (v) => settings.themeColorMode = v ? AvesThemeColorMode.polychrome : AvesThemeColorMode.monochrome, - title: context.l10n.settingsThemeColorHighlights, - ), - SettingsSelectionListTile( - values: DisplayRefreshRateMode.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.displayRefreshRateMode, - onSelection: (v) => settings.displayRefreshRateMode = v, - tileTitle: context.l10n.settingsDisplayRefreshRateModeTile, - dialogTitle: context.l10n.settingsDisplayRefreshRateModeTitle, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionDisplay; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileDisplayThemeBrightness(), + SettingsTileDisplayThemeColorMode(), + SettingsTileDisplayDisplayRefreshRateMode(), + ]; +} + +class SettingsTileDisplayThemeBrightness extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThemeBrightness; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: AvesThemeBrightness.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.themeBrightness, + onSelection: (v) => settings.themeBrightness = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsThemeBrightness, + ); +} + +class SettingsTileDisplayThemeColorMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThemeColorHighlights; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.themeColorMode == AvesThemeColorMode.polychrome, + onChanged: (v) => settings.themeColorMode = v ? AvesThemeColorMode.polychrome : AvesThemeColorMode.monochrome, + title: title(context), + ); +} + +class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsDisplayRefreshRateModeTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: DisplayRefreshRateMode.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.displayRefreshRateMode, + onSelection: (v) => settings.displayRefreshRateMode = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsDisplayRefreshRateModeTitle, + ); } diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart index 549766333..631981269 100644 --- a/lib/widgets/settings/language/language.dart +++ b/lib/widgets/settings/language/language.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/unit_system.dart'; @@ -6,57 +8,69 @@ import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/language/locale.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class LanguageSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const LanguageSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class LanguageSection extends SettingsSection { + @override + String get key => 'language'; @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return AvesExpansionTile( - // key is expected by test driver - key: const Key('section-language'), - // use a fixed value instead of the title to identify this expansion tile - // so that the tile state is kept when the language is modified - value: 'language', - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.language, color: context.select((v) => v.language), - ), - title: l10n.settingsSectionLanguage, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - const LocaleTile(), - SettingsSelectionListTile( - values: CoordinateFormat.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.coordinateFormat, - onSelection: (v) => settings.coordinateFormat = v, - tileTitle: l10n.settingsCoordinateFormatTile, - dialogTitle: l10n.settingsCoordinateFormatTitle, - optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo), - ), - SettingsSelectionListTile( - values: UnitSystem.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.unitSystem, - onSelection: (v) => settings.unitSystem = v, - tileTitle: l10n.settingsUnitSystemTile, - dialogTitle: l10n.settingsUnitSystemTitle, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionLanguage; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileLanguageLocale(), + SettingsTileLanguageCoordinateFormat(), + SettingsTileLanguageUnitSystem(), + ]; +} + +class SettingsTileLanguageLocale extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsLanguage; + + @override + Widget build(BuildContext context) => const LocaleTile(); +} + +class SettingsTileLanguageCoordinateFormat extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsCoordinateFormatTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: CoordinateFormat.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.coordinateFormat, + onSelection: (v) => settings.coordinateFormat = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsCoordinateFormatTitle, + optionSubtitleBuilder: (value) => value.format(context.l10n, Constants.pointNemo), + ); +} + +class SettingsTileLanguageUnitSystem extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsUnitSystemTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: UnitSystem.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.unitSystem, + onSelection: (v) => settings.unitSystem = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsUnitSystemTitle, + ); } diff --git a/lib/widgets/settings/navigation/confirmation_dialogs.dart b/lib/widgets/settings/navigation/confirmation_dialogs.dart index e45a7890f..4a09173f0 100644 --- a/lib/widgets/settings/navigation/confirmation_dialogs.dart +++ b/lib/widgets/settings/navigation/confirmation_dialogs.dart @@ -3,26 +3,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; -class ConfirmationDialogTile extends StatelessWidget { - const ConfirmationDialogTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsConfirmationDialogTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: ConfirmationDialogPage.routeName), - builder: (context) => const ConfirmationDialogPage(), - ), - ); - }, - ); - } -} - class ConfirmationDialogPage extends StatelessWidget { static const routeName = '/settings/navigation_confirmation'; diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index bd7adb4e2..cca5decaf 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -13,26 +13,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -class NavigationDrawerTile extends StatelessWidget { - const NavigationDrawerTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsNavigationDrawerTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: NavigationDrawerEditorPage.routeName), - builder: (context) => const NavigationDrawerEditorPage(), - ), - ); - }, - ); - } -} - class NavigationDrawerEditorPage extends StatefulWidget { static const routeName = '/settings/navigation_drawer'; diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 9602fb286..fa791b225 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + 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'; @@ -5,57 +7,99 @@ 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/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/navigation/confirmation_dialogs.dart'; import 'package:aves/widgets/settings/navigation/drawer.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class NavigationSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const NavigationSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class NavigationSection extends SettingsSection { + @override + String get key => 'navigation'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.home, color: context.select((v) => v.navigation), - ), - title: context.l10n.settingsSectionNavigation, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSelectionListTile( - values: HomePageSetting.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.homePage, - onSelection: (v) => settings.homePage = v, - tileTitle: context.l10n.settingsHome, - dialogTitle: context.l10n.settingsHome, - ), - const NavigationDrawerTile(), - const ConfirmationDialogTile(), - SettingsSelectionListTile( - values: KeepScreenOn.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.keepScreenOn, - onSelection: (v) => settings.keepScreenOn = v, - tileTitle: context.l10n.settingsKeepScreenOnTile, - dialogTitle: context.l10n.settingsKeepScreenOnTitle, - ), - SettingsSwitchListTile( - selector: (context, s) => s.mustBackTwiceToExit, - onChanged: (v) => settings.mustBackTwiceToExit = v, - title: context.l10n.settingsDoubleBackExit, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionNavigation; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileNavigationHomePage(), + SettingsTileNavigationDrawer(), + SettingsTileNavigationConfirmationDialog(), + SettingsTileNavigationKeepScreenOn(), + SettingsTileNavigationDoubleBackExit(), + ]; +} + +class SettingsTileNavigationHomePage extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsHome; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: HomePageSetting.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.homePage, + onSelection: (v) => settings.homePage = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsHome, + ); +} + +class SettingsTileNavigationDrawer extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsNavigationDrawerTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: NavigationDrawerEditorPage.routeName, + builder: (context) => const NavigationDrawerEditorPage(), + ); +} + +class SettingsTileNavigationConfirmationDialog extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsConfirmationDialogTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ConfirmationDialogPage.routeName, + builder: (context) => const ConfirmationDialogPage(), + ); +} + +class SettingsTileNavigationKeepScreenOn extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsKeepScreenOnTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: KeepScreenOn.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.keepScreenOn, + onSelection: (v) => settings.keepScreenOn = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsKeepScreenOnTitle, + ); +} + +class SettingsTileNavigationDoubleBackExit extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsDoubleBackExit; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.mustBackTwiceToExit, + onChanged: (v) => settings.mustBackTwiceToExit = v, + title: title(context), + ); } diff --git a/lib/widgets/settings/privacy/access_grants.dart b/lib/widgets/settings/privacy/access_grants.dart index 7355c4175..7376c631f 100644 --- a/lib/widgets/settings/privacy/access_grants.dart +++ b/lib/widgets/settings/privacy/access_grants.dart @@ -5,26 +5,6 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:flutter/material.dart'; -class StorageAccessTile extends StatelessWidget { - const StorageAccessTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsStorageAccessTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: StorageAccessPage.routeName), - builder: (context) => const StorageAccessPage(), - ), - ); - }, - ); - } -} - class StorageAccessPage extends StatefulWidget { static const routeName = '/settings/storage_access'; diff --git a/lib/widgets/settings/privacy/hidden_items.dart b/lib/widgets/settings/privacy/hidden_items.dart index 7073702d1..67b7c5996 100644 --- a/lib/widgets/settings/privacy/hidden_items.dart +++ b/lib/widgets/settings/privacy/hidden_items.dart @@ -14,26 +14,6 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class HiddenItemsTile extends StatelessWidget { - const HiddenItemsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsHiddenItemsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: HiddenItemsPage.routeName), - builder: (context) => const HiddenItemsPage(), - ), - ); - }, - ); - } -} - class HiddenItemsPage extends StatelessWidget { static const routeName = '/settings/hidden_items'; diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index b37bc1abb..c5be45b24 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -1,74 +1,126 @@ +import 'dart:async'; + import 'package:aves/app_flavor.dart'; 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'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/privacy/access_grants.dart'; import 'package:aves/widgets/settings/privacy/hidden_items.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class PrivacySection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const PrivacySection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class PrivacySection extends SettingsSection { + @override + String get key => 'privacy'; @override - Widget build(BuildContext context) { - final canEnableErrorReporting = context.select((v) => v.canEnableErrorReporting); - - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.privacy, color: context.select((v) => v.privacy), - ), - title: context.l10n.settingsSectionPrivacy, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSwitchListTile( - selector: (context, s) => s.isInstalledAppAccessAllowed, - onChanged: (v) => settings.isInstalledAppAccessAllowed = v, - title: context.l10n.settingsAllowInstalledAppAccess, - subtitle: context.l10n.settingsAllowInstalledAppAccessSubtitle, - ), - if (canEnableErrorReporting) - SettingsSwitchListTile( - selector: (context, s) => s.isErrorReportingAllowed, - onChanged: (v) => settings.isErrorReportingAllowed = v, - title: context.l10n.settingsAllowErrorReporting, - ), - SettingsSwitchListTile( - selector: (context, s) => s.saveSearchHistory, - onChanged: (v) { - settings.saveSearchHistory = v; - if (!v) { - settings.searchHistory = []; - } - }, - title: context.l10n.settingsSaveSearchHistory, - ), - SettingsSwitchListTile( - selector: (context, s) => s.enableBin, - onChanged: (v) { - settings.enableBin = v; - if (!v) { - settings.searchHistory = []; - } - }, - title: context.l10n.settingsEnableBin, - subtitle: context.l10n.settingsEnableBinSubtitle, - ), - const HiddenItemsTile(), - if (device.canGrantDirectoryAccess) const StorageAccessTile(), - ], - ); + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionPrivacy; + + @override + FutureOr> tiles(BuildContext context) async { + final canEnableErrorReporting = context.select((v) => v.canEnableErrorReporting); + return [ + SettingsTilePrivacyAllowInstalledAppAccess(), + if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(), + SettingsTilePrivacySaveSearchHistory(), + SettingsTilePrivacyEnableBin(), + SettingsTilePrivacyHiddenItems(), + if (device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(), + ]; } } + +class SettingsTilePrivacyAllowInstalledAppAccess extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsAllowInstalledAppAccess; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.isInstalledAppAccessAllowed, + onChanged: (v) => settings.isInstalledAppAccessAllowed = v, + title: title(context), + subtitle: context.l10n.settingsAllowInstalledAppAccessSubtitle, + ); +} + +class SettingsTilePrivacyAllowErrorReporting extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsAllowErrorReporting; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.isErrorReportingAllowed, + onChanged: (v) => settings.isErrorReportingAllowed = v, + title: title(context), + ); +} + +class SettingsTilePrivacySaveSearchHistory extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsSaveSearchHistory; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.saveSearchHistory, + onChanged: (v) { + settings.saveSearchHistory = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: title(context), + ); +} + +class SettingsTilePrivacyEnableBin extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsEnableBin; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableBin, + onChanged: (v) { + settings.enableBin = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: title(context), + subtitle: context.l10n.settingsEnableBinSubtitle, + ); +} + +class SettingsTilePrivacyHiddenItems extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsHiddenItemsTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: HiddenItemsPage.routeName, + builder: (context) => const HiddenItemsPage(), + ); +} + +class SettingsTilePrivacyStorageAccess extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsStorageAccessTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: StorageAccessPage.routeName, + builder: (context) => const StorageAccessPage(), + ); +} diff --git a/lib/widgets/settings/settings_definition.dart b/lib/widgets/settings/settings_definition.dart new file mode 100644 index 000000000..e0f7b7724 --- /dev/null +++ b/lib/widgets/settings/settings_definition.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:flutter/widgets.dart'; + +abstract class SettingsSection { + String get key; + + Widget icon(BuildContext context); + + String title(BuildContext context); + + FutureOr> tiles(BuildContext context); + + Widget build(BuildContext context, ValueNotifier expandedNotifier) { + return FutureBuilder>( + future: Future.value(tiles(context)), + builder: (context, snapshot) { + final tiles = snapshot.data; + if (tiles == null) return const SizedBox(); + + return AvesExpansionTile( + // key is expected by test driver + key: Key('section-$key'), + // use a fixed value instead of the title to identify this expansion tile + // so that the tile state is kept when the language is modified + value: key, + leading: icon(context), + title: title(context), + expandedNotifier: expandedNotifier, + showHighlight: false, + children: tiles.map((v) => v.build(context)).toList(), + ); + }, + ); + } +} + +abstract class SettingsTile { + String title(BuildContext context); + + Widget build(BuildContext context); +} diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 405d9ebe7..5442cd307 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -9,6 +9,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -21,6 +22,8 @@ import 'package:aves/widgets/settings/display/display.dart'; import 'package:aves/widgets/settings/language/language.dart'; import 'package:aves/widgets/settings/navigation/navigation.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves/widgets/settings/settings_search.dart'; import 'package:aves/widgets/settings/thumbnails/thumbnails.dart'; import 'package:aves/widgets/settings/video/video.dart'; import 'package:aves/widgets/settings/viewer/viewer.dart'; @@ -43,6 +46,17 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State with FeedbackMixin { final ValueNotifier _expandedNotifier = ValueNotifier(null); + static final List sections = [ + NavigationSection(), + ThumbnailsSection(), + ViewerSection(), + VideoSection(), + PrivacySection(), + AccessibilitySection(), + DisplaySection(), + LanguageSection(), + ]; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -50,8 +64,16 @@ class _SettingsPageState extends State with FeedbackMixin { return MediaQueryDataProvider( child: Scaffold( appBar: AppBar( - title: Text(context.l10n.settingsPageTitle), + title: InteractiveAppBarTitle( + onTap: () => _goToSearch(context), + child: Text(context.l10n.settingsPageTitle), + ), actions: [ + IconButton( + icon: const Icon(AIcons.search), + onPressed: () => _goToSearch(context), + tooltip: MaterialLocalizations.of(context).searchFieldLabel, + ), MenuIconTheme( child: PopupMenuButton( itemBuilder: (context) { @@ -74,6 +96,7 @@ class _SettingsPageState extends State with FeedbackMixin { ), ), ], + titleSpacing: 0, ), body: GestureAreaProtectorStack( child: SafeArea( @@ -100,16 +123,7 @@ class _SettingsPageState extends State with FeedbackMixin { child: child, ), ), - children: [ - NavigationSection(expandedNotifier: _expandedNotifier), - ThumbnailsSection(expandedNotifier: _expandedNotifier), - ViewerSection(expandedNotifier: _expandedNotifier), - VideoSection(expandedNotifier: _expandedNotifier), - PrivacySection(expandedNotifier: _expandedNotifier), - AccessibilitySection(expandedNotifier: _expandedNotifier), - DisplaySection(expandedNotifier: _expandedNotifier), - LanguageSection(expandedNotifier: _expandedNotifier), - ], + children: sections.map((v) => v.build(context, _expandedNotifier)).toList(), ), ); }), @@ -206,4 +220,14 @@ class _SettingsPageState extends State with FeedbackMixin { break; } } + + void _goToSearch(BuildContext context) { + showSearch( + context: context, + delegate: SettingsSearchDelegate( + searchFieldLabel: context.l10n.settingsSearchFieldLabel, + sections: sections, + ), + ); + } } diff --git a/lib/widgets/settings/settings_search.dart b/lib/widgets/settings/settings_search.dart new file mode 100644 index 000000000..a44c3bf94 --- /dev/null +++ b/lib/widgets/settings/settings_search.dart @@ -0,0 +1,114 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/animated_icons_fix.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/common/identity/highlight_title.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class SettingsSearchDelegate extends SearchDelegate { + final List sections; + + SettingsSearchDelegate({ + required String searchFieldLabel, + required this.sections, + }) : super( + searchFieldLabel: searchFieldLabel, + ); + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + // TODO TLAD [rtl] replace to regular `AnimatedIcon` when this is fixed: https://github.com/flutter/flutter/issues/60521 + icon: AnimatedIconFixIssue60521( + icon: AnimatedIconsFixIssue60521.menu_arrow, + progress: transitionAnimation, + ), + onPressed: () => Navigator.pop(context), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ); + } + + @override + List buildActions(BuildContext context) { + return [ + if (query.isNotEmpty) + IconButton( + icon: const Icon(AIcons.clear), + onPressed: () { + query = ''; + showSuggestions(context); + }, + tooltip: context.l10n.clearTooltip, + ), + ]; + } + + @override + Widget buildSuggestions(BuildContext context) => const SizedBox(); + + @override + Widget buildResults(BuildContext context) { + if (query.isEmpty) { + showSuggestions(context); + return const SizedBox(); + } + + final upQuery = query.toUpperCase().trim(); + + bool testKey(String key) => key.toUpperCase().contains(upQuery); + + final loader = Future.wait(sections.map((section) async { + final allTiles = await section.tiles(context); + final filteredTiles = testKey(section.title(context)) ? allTiles : allTiles.where((v) => testKey(v.title(context))).toList(); + if (filteredTiles.isEmpty) return null; + + return (context) { + return [ + Padding( + // match header layout in Settings page + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 13), + child: Row( + children: [ + section.icon(context), + const SizedBox(width: 8), + Expanded( + child: HighlightTitle( + title: section.title(context), + showHighlight: false, + ), + ), + ], + ), + ), + ...filteredTiles.map((v) => v.build(context)), + ]; + }; + })); + + return MediaQueryDataProvider( + child: SafeArea( + child: FutureBuilder Function(BuildContext)?>>( + future: loader, + builder: (context, snapshot) { + final loaders = snapshot.data; + if (loaders == null) return const SizedBox(); + + final children = loaders.whereNotNull().expand((builder) => builder(context)).toList(); + return children.isEmpty + ? EmptyContent( + icon: AIcons.settings, + text: context.l10n.settingsSearchEmpty, + ) + : ListView( + padding: const EdgeInsets.all(8), + children: children, + ); + }, + ), + ), + ); + } +} diff --git a/lib/widgets/settings/thumbnails/collection_actions_editor.dart b/lib/widgets/settings/thumbnails/collection_actions_editor.dart index a16a8977e..101bc13c1 100644 --- a/lib/widgets/settings/thumbnails/collection_actions_editor.dart +++ b/lib/widgets/settings/thumbnails/collection_actions_editor.dart @@ -6,26 +6,6 @@ import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -class CollectionActionsTile extends StatelessWidget { - const CollectionActionsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsCollectionQuickActionsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: CollectionActionEditorPage.routeName), - builder: (context) => const CollectionActionEditorPage(), - ), - ); - }, - ); - } -} - class CollectionActionEditorPage extends StatelessWidget { static const routeName = '/settings/collection_actions'; diff --git a/lib/widgets/settings/thumbnails/overlay.dart b/lib/widgets/settings/thumbnails/overlay.dart new file mode 100644 index 000000000..5de58497a --- /dev/null +++ b/lib/widgets/settings/thumbnails/overlay.dart @@ -0,0 +1,93 @@ +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/common/identity/aves_icons.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ThumbnailOverlayPage extends StatelessWidget { + static const routeName = '/settings/thumbnail_overlay'; + + const ThumbnailOverlayPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); + final iconColor = context.select((v) => v.neutral); + + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.settingsThumbnailOverlayTitle), + ), + body: SafeArea( + child: ListView( + children: [ + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailFavourite, + onChanged: (v) => settings.showThumbnailFavourite = v, + title: context.l10n.settingsThumbnailShowFavouriteIcon, + trailing: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - FavouriteIcon.scale) / 2), + child: Icon( + AIcons.favourite, + size: iconSize * FavouriteIcon.scale, + color: iconColor, + ), + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailLocation, + onChanged: (v) => settings.showThumbnailLocation = v, + title: context.l10n.settingsThumbnailShowLocationIcon, + trailing: Icon( + AIcons.location, + size: iconSize, + color: iconColor, + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailMotionPhoto, + onChanged: (v) => settings.showThumbnailMotionPhoto = v, + title: context.l10n.settingsThumbnailShowMotionPhotoIcon, + trailing: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), + child: Icon( + AIcons.motionPhoto, + size: iconSize * MotionPhotoIcon.scale, + color: iconColor, + ), + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailRating, + onChanged: (v) => settings.showThumbnailRating = v, + title: context.l10n.settingsThumbnailShowRating, + trailing: Icon( + AIcons.rating, + size: iconSize, + color: iconColor, + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailRaw, + onChanged: (v) => settings.showThumbnailRaw = v, + title: context.l10n.settingsThumbnailShowRawIcon, + trailing: Icon( + AIcons.raw, + size: iconSize, + color: iconColor, + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailVideoDuration, + onChanged: (v) => settings.showThumbnailVideoDuration = v, + title: context.l10n.settingsThumbnailShowVideoDuration, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index d12a14d4f..60f2c3729 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -1,99 +1,54 @@ -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/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/identity/aves_icons.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'; import 'package:aves/widgets/settings/thumbnails/collection_actions_editor.dart'; +import 'package:aves/widgets/settings/thumbnails/overlay.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class ThumbnailsSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const ThumbnailsSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class ThumbnailsSection extends SettingsSection { + @override + String get key => 'thumbnails'; @override - Widget build(BuildContext context) { - final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); - final iconColor = context.select((v) => v.neutral); - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.grid, color: context.select((v) => v.thumbnails), - ), - title: context.l10n.settingsSectionThumbnails, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - const CollectionActionsTile(), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailFavourite, - onChanged: (v) => settings.showThumbnailFavourite = v, - title: context.l10n.settingsThumbnailShowFavouriteIcon, - trailing: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - FavouriteIcon.scale) / 2), - child: Icon( - AIcons.favourite, - size: iconSize * FavouriteIcon.scale, - color: iconColor, - ), - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailLocation, - onChanged: (v) => settings.showThumbnailLocation = v, - title: context.l10n.settingsThumbnailShowLocationIcon, - trailing: Icon( - AIcons.location, - size: iconSize, - color: iconColor, - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailMotionPhoto, - onChanged: (v) => settings.showThumbnailMotionPhoto = v, - title: context.l10n.settingsThumbnailShowMotionPhotoIcon, - trailing: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), - child: Icon( - AIcons.motionPhoto, - size: iconSize * MotionPhotoIcon.scale, - color: iconColor, - ), - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailRating, - onChanged: (v) => settings.showThumbnailRating = v, - title: context.l10n.settingsThumbnailShowRating, - trailing: Icon( - AIcons.rating, - size: iconSize, - color: iconColor, - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailRaw, - onChanged: (v) => settings.showThumbnailRaw = v, - title: context.l10n.settingsThumbnailShowRawIcon, - trailing: Icon( - AIcons.raw, - size: iconSize, - color: iconColor, - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailVideoDuration, - onChanged: (v) => settings.showThumbnailVideoDuration = v, - title: context.l10n.settingsThumbnailShowVideoDuration, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionThumbnails; + + @override + List tiles(BuildContext context) => [ + SettingsTileCollectionQuickActions(), + SettingsTileThumbnailOverlay(), + ]; +} + +class SettingsTileCollectionQuickActions extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsCollectionQuickActionsTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: CollectionActionEditorPage.routeName, + builder: (context) => const CollectionActionEditorPage(), + ); +} + +class SettingsTileThumbnailOverlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThumbnailOverlayTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ThumbnailOverlayPage.routeName, + builder: (context) => const ThumbnailOverlayPage(), + ); } diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart index 1263508bb..2f341c86e 100644 --- a/lib/widgets/settings/video/controls.dart +++ b/lib/widgets/settings/video/controls.dart @@ -5,26 +5,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; -class VideoControlsTile extends StatelessWidget { - const VideoControlsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsVideoControlsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: VideoControlsPage.routeName), - builder: (context) => const VideoControlsPage(), - ), - ); - }, - ); - } -} - class VideoControlsPage extends StatelessWidget { static const routeName = '/settings/video/controls'; diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index 444c65456..fc5117870 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -7,26 +7,6 @@ import 'package:aves/widgets/settings/video/subtitle_sample.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class SubtitleThemeTile extends StatelessWidget { - const SubtitleThemeTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsSubtitleThemeTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: SubtitleThemePage.routeName), - builder: (context) => const SubtitleThemePage(), - ), - ); - }, - ); - } -} - class SubtitleThemePage extends StatelessWidget { static const routeName = '/settings/video/subtitle_theme'; diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index 4db331bc1..4e5636c5a 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/video_loop_mode.dart'; @@ -5,100 +7,117 @@ 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/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/providers/media_query_data_provider.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'; import 'package:aves/widgets/settings/video/controls.dart'; import 'package:aves/widgets/settings/video/subtitle_theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class VideoSection extends StatelessWidget { - final ValueNotifier? expandedNotifier; +class VideoSection extends SettingsSection { final bool standalonePage; - const VideoSection({ - Key? key, - this.expandedNotifier, + VideoSection({ this.standalonePage = false, - }) : super(key: key); + }); @override - Widget build(BuildContext context) { - final children = [ - if (!standalonePage) - SettingsSwitchListTile( - selector: (context, s) => !s.hiddenFilters.contains(MimeFilter.video), - onChanged: (v) => settings.changeFilterVisibility({MimeFilter.video}, v), - title: context.l10n.settingsVideoShowVideos, - ), - SettingsSwitchListTile( + String get key => 'video'; + + @override + Widget icon(BuildContext context) => SettingsTileLeading( + icon: AIcons.video, + color: context.select((v) => v.video), + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionVideo; + + @override + FutureOr> tiles(BuildContext context) async { + return [ + if (!standalonePage) SettingsTileVideoShowVideos(), + SettingsTileVideoEnableHardwareAcceleration(), + SettingsTileVideoEnableAutoPlay(), + SettingsTileVideoLoopMode(), + SettingsTileVideoControls(), + SettingsTileVideoSubtitleTheme(), + ]; + } +} + +class SettingsTileVideoShowVideos extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoShowVideos; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => !s.hiddenFilters.contains(MimeFilter.video), + onChanged: (v) => settings.changeFilterVisibility({MimeFilter.video}, v), + title: title(context), + ); +} + +class SettingsTileVideoEnableHardwareAcceleration extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoEnableHardwareAcceleration; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( selector: (context, s) => s.enableVideoHardwareAcceleration, onChanged: (v) => settings.enableVideoHardwareAcceleration = v, - title: context.l10n.settingsVideoEnableHardwareAcceleration, - ), - SettingsSwitchListTile( + title: title(context), + ); +} + +class SettingsTileVideoEnableAutoPlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoEnableAutoPlay; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( selector: (context, s) => s.enableVideoAutoPlay, onChanged: (v) => settings.enableVideoAutoPlay = v, - title: context.l10n.settingsVideoEnableAutoPlay, - ), - SettingsSelectionListTile( + title: title(context), + ); +} + +class SettingsTileVideoLoopMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoLoopModeTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( values: VideoLoopMode.values, getName: (context, v) => v.getName(context), selector: (context, s) => s.videoLoopMode, onSelection: (v) => settings.videoLoopMode = v, - tileTitle: context.l10n.settingsVideoLoopModeTile, + tileTitle: title(context), dialogTitle: context.l10n.settingsVideoLoopModeTitle, - ), - const VideoControlsTile(), - const SubtitleThemeTile(), - ]; - - return standalonePage - ? ListView( - children: children, - ) - : AvesExpansionTile( - leading: SettingsTileLeading( - icon: AIcons.video, - color: context.select((v) => v.video), - ), - title: context.l10n.settingsSectionVideo, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: children, - ); - } + ); } -class VideoSettingsPage extends StatelessWidget { - static const routeName = '/settings/video'; - - const VideoSettingsPage({Key? key}) : super(key: key); +class SettingsTileVideoControls extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoControlsTile; @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return MediaQueryDataProvider( - child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.settingsVideoPageTitle), - ), - body: Theme( - data: theme.copyWith( - textTheme: theme.textTheme.copyWith( - // dense style font for tile subtitles, without modifying title font - bodyText2: const TextStyle(fontSize: 12), - ), - ), - child: const SafeArea( - child: VideoSection( - standalonePage: true, - ), - ), - ), - ), - ); - } + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: VideoControlsPage.routeName, + builder: (context) => const VideoControlsPage(), + ); +} + +class SettingsTileVideoSubtitleTheme extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsSubtitleThemeTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: SubtitleThemePage.routeName, + builder: (context) => const SubtitleThemePage(), + ); } diff --git a/lib/widgets/settings/video/video_settings_page.dart b/lib/widgets/settings/video/video_settings_page.dart new file mode 100644 index 000000000..ed63554e4 --- /dev/null +++ b/lib/widgets/settings/video/video_settings_page.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves/widgets/settings/video/video.dart'; +import 'package:flutter/material.dart'; + +class VideoSettingsPage extends StatefulWidget { + static const routeName = '/settings/video'; + + const VideoSettingsPage({Key? key}) : super(key: key); + + @override + State createState() => _VideoSettingsPageState(); +} + +class _VideoSettingsPageState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + title: Text(context.l10n.settingsVideoPageTitle), + ), + body: Theme( + data: theme.copyWith( + textTheme: theme.textTheme.copyWith( + // dense style font for tile subtitles, without modifying title font + bodyText2: const TextStyle(fontSize: 12), + ), + ), + child: SafeArea( + child: FutureBuilder>( + future: Future.value(VideoSection(standalonePage: true).tiles(context)), + builder: (context, snapshot) { + final tiles = snapshot.data; + if (tiles == null) return const SizedBox(); + + return ListView( + children: tiles.map((v) => v.build(context)).toList(), + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index 463fa97e9..66b1c4e41 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -5,26 +5,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class ViewerOverlayTile extends StatelessWidget { - const ViewerOverlayTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsViewerOverlayTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: ViewerOverlayPage.routeName), - builder: (context) => const ViewerOverlayPage(), - ), - ); - }, - ); - } -} - class ViewerOverlayPage extends StatelessWidget { static const routeName = '/settings/viewer_overlay'; diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 5f993816a..35176bd92 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -1,95 +1,120 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.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/common/identity/aves_expansion_tile.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'; import 'package:aves/widgets/settings/viewer/entry_background.dart'; import 'package:aves/widgets/settings/viewer/overlay.dart'; import 'package:aves/widgets/settings/viewer/viewer_actions_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class ViewerSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const ViewerSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class ViewerSection extends SettingsSection { + @override + String get key => 'viewer'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.image, color: context.select((v) => v.image), - ), - title: context.l10n.settingsSectionViewer, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - const ViewerActionsTile(), - const ViewerOverlayTile(), - const _CutoutModeSwitch(), - SettingsSwitchListTile( - selector: (context, s) => s.viewerMaxBrightness, - onChanged: (v) => settings.viewerMaxBrightness = v, - title: context.l10n.settingsViewerMaximumBrightness, - ), - SettingsSwitchListTile( - selector: (context, s) => s.enableMotionPhotoAutoPlay, - onChanged: (v) => settings.enableMotionPhotoAutoPlay = v, - title: context.l10n.settingsMotionPhotoAutoPlay, - ), - Selector( - selector: (context, s) => s.imageBackground, - builder: (context, current, child) => ListTile( - title: Text(context.l10n.settingsImageBackground), - trailing: EntryBackgroundSelector( - getter: () => current, - setter: (value) => settings.imageBackground = value, - ), + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionViewer; + + @override + FutureOr> tiles(BuildContext context) async { + final canSetCutoutMode = await windowService.canSetCutoutMode(); + return [ + SettingsTileViewerQuickActions(), + SettingsTileViewerOverlay(), + if (canSetCutoutMode) SettingsTileViewerCutoutMode(), + SettingsTileViewerMaxBrightness(), + SettingsTileViewerMotionPhotoAutoPlay(), + SettingsTileViewerImageBackground(), + ]; + } +} + +class SettingsTileViewerQuickActions extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerQuickActionsTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ViewerActionEditorPage.routeName, + builder: (context) => const ViewerActionEditorPage(), + ); +} + +class SettingsTileViewerOverlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerOverlayTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ViewerOverlayPage.routeName, + builder: (context) => const ViewerOverlayPage(), + ); +} + +class SettingsTileViewerCutoutMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerUseCutout; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.viewerUseCutout, + onChanged: (v) => settings.viewerUseCutout = v, + title: title(context), + ); +} + +class SettingsTileViewerMaxBrightness extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerMaximumBrightness; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.viewerMaxBrightness, + onChanged: (v) => settings.viewerMaxBrightness = v, + title: title(context), + ); +} + +class SettingsTileViewerMotionPhotoAutoPlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsMotionPhotoAutoPlay; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableMotionPhotoAutoPlay, + onChanged: (v) => settings.enableMotionPhotoAutoPlay = v, + title: title(context), + ); +} + +class SettingsTileViewerImageBackground extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsImageBackground; + + @override + Widget build(BuildContext context) => Selector( + selector: (context, s) => s.imageBackground, + builder: (context, current, child) => ListTile( + title: Text(title(context)), + trailing: EntryBackgroundSelector( + getter: () => current, + setter: (value) => settings.imageBackground = value, ), ), - ], - ); - } -} - -class _CutoutModeSwitch extends StatefulWidget { - const _CutoutModeSwitch({Key? key}) : super(key: key); - - @override - State<_CutoutModeSwitch> createState() => _CutoutModeSwitchState(); -} - -class _CutoutModeSwitchState extends State<_CutoutModeSwitch> { - late Future _canSet; - - @override - void initState() { - super.initState(); - _canSet = windowService.canSetCutoutMode(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _canSet, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!) { - return SettingsSwitchListTile( - selector: (context, s) => s.viewerUseCutout, - onChanged: (v) => settings.viewerUseCutout = v, - title: context.l10n.settingsViewerUseCutout, - ); - } - return const SizedBox.shrink(); - }, - ); - } + ); } diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index 453a58f96..6ae06565d 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -4,26 +4,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; -class ViewerActionsTile extends StatelessWidget { - const ViewerActionsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsViewerQuickActionsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: ViewerActionEditorPage.routeName), - builder: (context) => const ViewerActionEditorPage(), - ), - ); - }, - ); - } -} - class ViewerActionEditorPage extends StatelessWidget { static const routeName = '/settings/viewer_actions'; diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart index 0fc98c622..3af7d520b 100644 --- a/lib/widgets/viewer/video_action_delegate.dart +++ b/lib/widgets/viewer/video_action_delegate.dart @@ -15,7 +15,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/video_speed_dialog.dart'; import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart'; -import 'package:aves/widgets/settings/video/video.dart'; +import 'package:aves/widgets/settings/video/video_settings_page.dart'; import 'package:aves/widgets/viewer/overlay/notifications.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:collection/collection.dart'; diff --git a/untranslated.json b/untranslated.json index 1495c4f1e..011efe8f2 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,4 +1,11 @@ { + "de": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + "es": [ "entryActionShowGeoTiffOnMap", "entryActionConvertMotionPhotoToStillImage", @@ -9,7 +16,32 @@ "coverDialogTabApp", "coverDialogTabColor", "appPickDialogTitle", - "appPickDialogNone" + "appPickDialogNone", + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + + "fr": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + + "id": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + + "it": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" ], "ja": [ @@ -22,6 +54,38 @@ "coverDialogTabApp", "coverDialogTabColor", "appPickDialogTitle", - "appPickDialogNone" + "appPickDialogNone", + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + + "ko": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + + "pt": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + + "ru": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" + ], + + "zh": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayTitle" ] }