From 4345f46cc2cdc8777e91d6043d56dacf6dc19ccf Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 24 Jun 2021 11:14:54 +0900 Subject: [PATCH] improved settings rebuild --- lib/l10n/app_en.arb | 17 + lib/model/settings/settings.dart | 38 +- lib/theme/durations.dart | 1 + lib/widgets/common/basic/outlined_text.dart | 29 +- .../quick_actions/action_button.dart | 0 .../quick_actions/action_panel.dart | 0 .../quick_actions/available_actions.dart | 4 +- .../quick_actions/editor_page.dart | 10 +- .../quick_actions/placeholder.dart | 0 .../quick_actions/quick_actions.dart | 0 lib/widgets/settings/common/tile_leading.dart | 34 ++ lib/widgets/settings/language/language.dart | 61 +++ .../{language.dart => language/locale.dart} | 3 +- lib/widgets/settings/navigation.dart | 79 ++++ .../settings/{ => privacy}/access_grants.dart | 0 .../{ => privacy}/hidden_filters.dart | 9 +- lib/widgets/settings/privacy/privacy.dart | 54 +++ lib/widgets/settings/settings_page.dart | 333 ++------------ lib/widgets/settings/thumbnails.dart | 79 ++++ lib/widgets/settings/video/video.dart | 76 ++++ .../{ => video}/video_actions_editor.dart | 2 +- .../{ => viewer}/entry_background.dart | 0 lib/widgets/settings/viewer/viewer.dart | 72 ++++ .../{ => viewer}/viewer_actions_editor.dart | 2 +- .../viewer/visual/subtitle/ass_parser.dart | 2 +- .../viewer/visual/subtitle/subtitle.dart | 406 +++++++++--------- 26 files changed, 771 insertions(+), 540 deletions(-) rename lib/widgets/settings/{ => common}/quick_actions/action_button.dart (100%) rename lib/widgets/settings/{ => common}/quick_actions/action_panel.dart (100%) rename lib/widgets/settings/{ => common}/quick_actions/available_actions.dart (95%) rename lib/widgets/settings/{ => common}/quick_actions/editor_page.dart (96%) rename lib/widgets/settings/{ => common}/quick_actions/placeholder.dart (100%) rename lib/widgets/settings/{ => common}/quick_actions/quick_actions.dart (100%) create mode 100644 lib/widgets/settings/common/tile_leading.dart create mode 100644 lib/widgets/settings/language/language.dart rename lib/widgets/settings/{language.dart => language/locale.dart} (96%) create mode 100644 lib/widgets/settings/navigation.dart rename lib/widgets/settings/{ => privacy}/access_grants.dart (100%) rename lib/widgets/settings/{ => privacy}/hidden_filters.dart (91%) create mode 100644 lib/widgets/settings/privacy/privacy.dart create mode 100644 lib/widgets/settings/thumbnails.dart create mode 100644 lib/widgets/settings/video/video.dart rename lib/widgets/settings/{ => video}/video_actions_editor.dart (94%) rename lib/widgets/settings/{ => viewer}/entry_background.dart (100%) create mode 100644 lib/widgets/settings/viewer/viewer.dart rename lib/widgets/settings/{ => viewer}/viewer_actions_editor.dart (95%) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ac51ffb20..345224fdc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -597,6 +597,23 @@ "settingsVideoQuickActionEditorTitle": "Quick Video Actions", "@settingsVideoQuickActionEditorTitle": {}, + "settingsSubtitleThemeTile": "Subtitles", + "@settingsSubtitleThemeTile": {}, + "settingsSubtitleThemeTitle": "Subtitles", + "@settingsSubtitleThemeTitle": {}, + "settingsSubtitleThemeSample": "This is a sample.", + "@settingsSubtitleThemeSample": {}, + "settingsSubtitleThemeTextAlignmentTile": "Text alignment", + "@settingsSubtitleThemeTextAlignmentTile": {}, + "settingsSubtitleThemeTextAlignmentTitle": "Text Alignment", + "@settingsSubtitleThemeTextAlignmentTitle": {}, + "settingsSubtitleThemeTextAlignmentLeft": "Left", + "@settingsSubtitleThemeTextAlignmentLeft": {}, + "settingsSubtitleThemeTextAlignmentCenter": "Center", + "@settingsSubtitleThemeTextAlignmentCenter": {}, + "settingsSubtitleThemeTextAlignmentRight": "Right", + "@settingsSubtitleThemeTextAlignmentRight": {}, + "settingsSectionPrivacy": "Privacy", "@settingsSectionPrivacy": {}, "settingsEnableAnalytics": "Allow anonymous analytics and crash reporting", diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 560c35a86..64cf880b2 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -8,6 +8,7 @@ import 'package:collection/collection.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:pedantic/pedantic.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -57,6 +58,13 @@ class Settings extends ChangeNotifier { static const videoLoopModeKey = 'video_loop'; static const videoShowRawTimedTextKey = 'video_show_raw_timed_text'; + // subtitles + static const subtitleFontSizeKey = 'subtitle_font_size'; + static const subtitleTextAlignmentKey = 'subtitle_text_alignment'; + static const subtitleShowOutlineKey = 'subtitle_show_outline'; + static const subtitleTextColorKey = 'subtitle_text_color'; + static const subtitleBackgroundColorKey = 'subtitle_background_color'; + // info static const infoMapStyleKey = 'info_map_style'; static const infoMapZoomKey = 'info_map_zoom'; @@ -241,21 +249,43 @@ class Settings extends ChangeNotifier { // video - set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue); - bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, true); - set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue); + set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue); bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, false); + set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue); + VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values); set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString()); + bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false); + set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue); - bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false); + // subtitles + + double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? 20; + + set subtitleFontSize(double newValue) => setAndNotify(subtitleFontSizeKey, newValue); + + TextAlign get subtitleTextAlignment => getEnumOrDefault(subtitleTextAlignmentKey, TextAlign.center, TextAlign.values); + + set subtitleTextAlignment(TextAlign newValue) => setAndNotify(subtitleTextAlignmentKey, newValue.toString()); + + bool get subtitleShowOutline => getBoolOrDefault(subtitleShowOutlineKey, true); + + set subtitleShowOutline(bool newValue) => setAndNotify(subtitleShowOutlineKey, newValue); + + Color get subtitleTextColor => Color(_prefs!.getInt(subtitleTextColorKey) ?? Colors.white.value); + + set subtitleTextColor(Color newValue) => setAndNotify(subtitleTextColorKey, newValue.value); + + Color get subtitleBackgroundColor => Color(_prefs!.getInt(subtitleBackgroundColorKey) ?? Colors.transparent.value); + + set subtitleBackgroundColor(Color newValue) => setAndNotify(subtitleBackgroundColorKey, newValue.value); // info diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 1f85b0f8c..357b58415 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -52,6 +52,7 @@ class Durations { // settings animations static const quickActionListAnimation = Duration(milliseconds: 200); static const quickActionHighlightAnimation = Duration(milliseconds: 200); + static const themeChangeDuration = Duration(milliseconds: 400); // delays & refresh intervals static const opToastDisplay = Duration(seconds: 3); diff --git a/lib/widgets/common/basic/outlined_text.dart b/lib/widgets/common/basic/outlined_text.dart index 81e22985f..18f1aef28 100644 --- a/lib/widgets/common/basic/outlined_text.dart +++ b/lib/widgets/common/basic/outlined_text.dart @@ -27,22 +27,23 @@ class OutlinedText extends StatelessWidget { Widget build(BuildContext context) { return Stack( children: [ - ImageFiltered( - imageFilter: outlineBlurSigma > 0 - ? ImageFilter.blur( - sigmaX: outlineBlurSigma, - sigmaY: outlineBlurSigma, - ) - : ImageFilter.matrix( - Matrix4.identity().storage, - ), - child: Text.rich( - TextSpan( - children: textSpans.map(_toStrokeSpan).toList(), + if (outlineWidth > 0) + ImageFiltered( + imageFilter: outlineBlurSigma > 0 + ? ImageFilter.blur( + sigmaX: outlineBlurSigma, + sigmaY: outlineBlurSigma, + ) + : ImageFilter.matrix( + Matrix4.identity().storage, + ), + child: Text.rich( + TextSpan( + children: textSpans.map(_toStrokeSpan).toList(), + ), + textAlign: textAlign, ), - textAlign: textAlign, ), - ), Text.rich( TextSpan( children: textSpans, diff --git a/lib/widgets/settings/quick_actions/action_button.dart b/lib/widgets/settings/common/quick_actions/action_button.dart similarity index 100% rename from lib/widgets/settings/quick_actions/action_button.dart rename to lib/widgets/settings/common/quick_actions/action_button.dart diff --git a/lib/widgets/settings/quick_actions/action_panel.dart b/lib/widgets/settings/common/quick_actions/action_panel.dart similarity index 100% rename from lib/widgets/settings/quick_actions/action_panel.dart rename to lib/widgets/settings/common/quick_actions/action_panel.dart diff --git a/lib/widgets/settings/quick_actions/available_actions.dart b/lib/widgets/settings/common/quick_actions/available_actions.dart similarity index 95% rename from lib/widgets/settings/quick_actions/available_actions.dart rename to lib/widgets/settings/common/quick_actions/available_actions.dart index 0ec23c24d..602774c00 100644 --- a/lib/widgets/settings/quick_actions/available_actions.dart +++ b/lib/widgets/settings/common/quick_actions/available_actions.dart @@ -1,6 +1,6 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; -import 'package:aves/widgets/settings/quick_actions/action_button.dart'; -import 'package:aves/widgets/settings/quick_actions/placeholder.dart'; +import 'package:aves/widgets/settings/common/quick_actions/action_button.dart'; +import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; import 'package:flutter/widgets.dart'; class AvailableActionPanel extends StatelessWidget { diff --git a/lib/widgets/settings/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart similarity index 96% rename from lib/widgets/settings/quick_actions/editor_page.dart rename to lib/widgets/settings/common/quick_actions/editor_page.dart index 14750be85..f36f22888 100644 --- a/lib/widgets/settings/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -6,11 +6,11 @@ import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/constants.dart'; 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/quick_actions/action_button.dart'; -import 'package:aves/widgets/settings/quick_actions/action_panel.dart'; -import 'package:aves/widgets/settings/quick_actions/available_actions.dart'; -import 'package:aves/widgets/settings/quick_actions/placeholder.dart'; -import 'package:aves/widgets/settings/quick_actions/quick_actions.dart'; +import 'package:aves/widgets/settings/common/quick_actions/action_button.dart'; +import 'package:aves/widgets/settings/common/quick_actions/action_panel.dart'; +import 'package:aves/widgets/settings/common/quick_actions/available_actions.dart'; +import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; +import 'package:aves/widgets/settings/common/quick_actions/quick_actions.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/settings/quick_actions/placeholder.dart b/lib/widgets/settings/common/quick_actions/placeholder.dart similarity index 100% rename from lib/widgets/settings/quick_actions/placeholder.dart rename to lib/widgets/settings/common/quick_actions/placeholder.dart diff --git a/lib/widgets/settings/quick_actions/quick_actions.dart b/lib/widgets/settings/common/quick_actions/quick_actions.dart similarity index 100% rename from lib/widgets/settings/quick_actions/quick_actions.dart rename to lib/widgets/settings/common/quick_actions/quick_actions.dart diff --git a/lib/widgets/settings/common/tile_leading.dart b/lib/widgets/settings/common/tile_leading.dart new file mode 100644 index 000000000..4f503f818 --- /dev/null +++ b/lib/widgets/settings/common/tile_leading.dart @@ -0,0 +1,34 @@ +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/material.dart'; + +class SettingsTileLeading extends StatelessWidget { + final IconData icon; + final Color color; + + const SettingsTileLeading({ + Key? key, + required this.icon, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: color, + width: AvesFilterChip.outlineWidth, + )), + shape: BoxShape.circle, + ), + child: DecoratedIcon( + icon, + shadows: Constants.embossShadows, + size: 18, + ), + ); + } +} diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart new file mode 100644 index 000000000..d9526106e --- /dev/null +++ b/lib/widgets/settings/language/language.dart @@ -0,0 +1,61 @@ +import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/color_utils.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/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/language/locale.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); + + @override + Widget build(BuildContext context) { + final currentCoordinateFormat = context.select((s) => s.coordinateFormat); + + return AvesExpansionTile( + // 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( + icon: AIcons.language, + color: stringToColor('Language'), + ), + title: context.l10n.settingsSectionLanguage, + expandedNotifier: expandedNotifier, + showHighlight: false, + children: [ + LocaleTile(), + ListTile( + title: Text(context.l10n.settingsCoordinateFormatTile), + subtitle: Text(currentCoordinateFormat.getName(context)), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentCoordinateFormat, + options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))), + optionSubtitleBuilder: (value) => value.format(Constants.pointNemo), + title: context.l10n.settingsCoordinateFormatTitle, + ), + ); + if (value != null) { + settings.coordinateFormat = value; + } + }, + ), + ], + ); + } +} diff --git a/lib/widgets/settings/language.dart b/lib/widgets/settings/language/locale.dart similarity index 96% rename from lib/widgets/settings/language.dart rename to lib/widgets/settings/language/locale.dart index e79f6eaea..a50768683 100644 --- a/lib/widgets/settings/language.dart +++ b/lib/widgets/settings/language/locale.dart @@ -7,10 +7,9 @@ import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class LanguageTile extends StatelessWidget { +class LocaleTile extends StatelessWidget { static const _systemLocaleOption = Locale('system'); @override diff --git a/lib/widgets/settings/navigation.dart b/lib/widgets/settings/navigation.dart new file mode 100644 index 000000000..9edd83c1d --- /dev/null +++ b/lib/widgets/settings/navigation.dart @@ -0,0 +1,79 @@ +import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/home_page.dart'; +import 'package:aves/model/settings/screen_on.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/color_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/settings/common/tile_leading.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); + + @override + Widget build(BuildContext context) { + final currentHomePage = context.select((s) => s.homePage); + final currentKeepScreenOn = context.select((s) => s.keepScreenOn); + final currentMustBackTwiceToExit = context.select((s) => s.mustBackTwiceToExit); + + return AvesExpansionTile( + leading: SettingsTileLeading( + icon: AIcons.home, + color: stringToColor('Navigation'), + ), + title: context.l10n.settingsSectionNavigation, + expandedNotifier: expandedNotifier, + showHighlight: false, + children: [ + ListTile( + title: Text(context.l10n.settingsHome), + subtitle: Text(currentHomePage.getName(context)), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentHomePage, + options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsHome, + ), + ); + if (value != null) { + settings.homePage = value; + } + }, + ), + ListTile( + title: Text(context.l10n.settingsKeepScreenOnTile), + subtitle: Text(currentKeepScreenOn.getName(context)), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentKeepScreenOn, + options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsKeepScreenOnTitle, + ), + ); + if (value != null) { + settings.keepScreenOn = value; + } + }, + ), + SwitchListTile( + value: currentMustBackTwiceToExit, + onChanged: (v) => settings.mustBackTwiceToExit = v, + title: Text(context.l10n.settingsDoubleBackExit), + ), + ], + ); + } +} diff --git a/lib/widgets/settings/access_grants.dart b/lib/widgets/settings/privacy/access_grants.dart similarity index 100% rename from lib/widgets/settings/access_grants.dart rename to lib/widgets/settings/privacy/access_grants.dart diff --git a/lib/widgets/settings/hidden_filters.dart b/lib/widgets/settings/privacy/hidden_filters.dart similarity index 91% rename from lib/widgets/settings/hidden_filters.dart rename to lib/widgets/settings/privacy/hidden_filters.dart index 6e5a71f55..0d7ab2768 100644 --- a/lib/widgets/settings/hidden_filters.dart +++ b/lib/widgets/settings/privacy/hidden_filters.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; @@ -52,16 +53,16 @@ class HiddenFilterPage extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.all(8), - child: Consumer( - builder: (context, settings, child) { - final hiddenFilters = settings.hiddenFilters; - final filterList = hiddenFilters.toList()..sort(); + child: Selector>( + selector: (context, s) => settings.hiddenFilters, + builder: (context, hiddenFilters, child) { if (hiddenFilters.isEmpty) { return EmptyContent( icon: AIcons.hide, text: context.l10n.settingsHiddenFiltersEmpty, ); } + final filterList = hiddenFilters.toList()..sort(); return Wrap( spacing: 8, runSpacing: 8, diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart new file mode 100644 index 000000000..3babb19e4 --- /dev/null +++ b/lib/widgets/settings/privacy/privacy.dart @@ -0,0 +1,54 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/color_utils.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/privacy/access_grants.dart'; +import 'package:aves/widgets/settings/privacy/hidden_filters.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); + + @override + Widget build(BuildContext context) { + final currentIsCrashlyticsEnabled = context.select((s) => s.isCrashlyticsEnabled); + final currentSaveSearchHistory = context.select((s) => s.saveSearchHistory); + + return AvesExpansionTile( + leading: SettingsTileLeading( + icon: AIcons.privacy, + color: stringToColor('Privacy'), + ), + title: context.l10n.settingsSectionPrivacy, + expandedNotifier: expandedNotifier, + showHighlight: false, + children: [ + SwitchListTile( + value: currentIsCrashlyticsEnabled, + onChanged: (v) => settings.isCrashlyticsEnabled = v, + title: Text(context.l10n.settingsEnableAnalytics), + ), + SwitchListTile( + value: currentSaveSearchHistory, + onChanged: (v) { + settings.saveSearchHistory = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: Text(context.l10n.settingsSaveSearchHistory), + ), + HiddenFilterTile(), + StorageAccessTile(), + ], + ); + } +} diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 25f969b2e..55177c2b6 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -1,30 +1,14 @@ -import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; -import 'package:aves/model/settings/enums.dart'; -import 'package:aves/model/settings/home_page.dart'; -import 'package:aves/model/settings/screen_on.dart'; -import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/settings/video_loop_mode.dart'; -import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.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/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/settings/access_grants.dart'; -import 'package:aves/widgets/settings/entry_background.dart'; -import 'package:aves/widgets/settings/hidden_filters.dart'; -import 'package:aves/widgets/settings/language.dart'; -import 'package:aves/widgets/settings/video_actions_editor.dart'; -import 'package:aves/widgets/settings/viewer_actions_editor.dart'; -import 'package:decorated_icon/decorated_icon.dart'; +import 'package:aves/widgets/settings/language/language.dart'; +import 'package:aves/widgets/settings/navigation.dart'; +import 'package:aves/widgets/settings/privacy/privacy.dart'; +import 'package:aves/widgets/settings/thumbnails.dart'; +import 'package:aves/widgets/settings/video/video.dart'; +import 'package:aves/widgets/settings/viewer/viewer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:provider/provider.dart'; class SettingsPage extends StatefulWidget { static const routeName = '/settings'; @@ -52,28 +36,26 @@ class _SettingsPageState extends State { ), ), child: SafeArea( - child: Consumer( - builder: (context, settings, child) => AnimationLimiter( - child: ListView( - padding: const EdgeInsets.all(8), - children: AnimationConfiguration.toStaggeredList( - duration: Durations.staggeredAnimation, - delay: Durations.staggeredAnimationDelay, - childAnimationBuilder: (child) => SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, - ), + child: AnimationLimiter( + child: ListView( + padding: const EdgeInsets.all(8), + children: AnimationConfiguration.toStaggeredList( + duration: Durations.staggeredAnimation, + delay: Durations.staggeredAnimationDelay, + childAnimationBuilder: (child) => SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, ), - children: [ - _buildNavigationSection(context), - _buildThumbnailsSection(context), - _buildViewerSection(context), - _buildVideoSection(context), - _buildPrivacySection(context), - _buildLanguageSection(context), - ], ), + children: [ + NavigationSection(expandedNotifier: _expandedNotifier), + ThumbnailsSection(expandedNotifier: _expandedNotifier), + ViewerSection(expandedNotifier: _expandedNotifier), + VideoSection(expandedNotifier: _expandedNotifier), + PrivacySection(expandedNotifier: _expandedNotifier), + LanguageSection(expandedNotifier: _expandedNotifier), + ], ), ), ), @@ -82,271 +64,4 @@ class _SettingsPageState extends State { ), ); } - - Widget _buildNavigationSection(BuildContext context) { - return AvesExpansionTile( - leading: _buildLeading(AIcons.home, stringToColor('Navigation')), - title: context.l10n.settingsSectionNavigation, - expandedNotifier: _expandedNotifier, - showHighlight: false, - children: [ - ListTile( - title: Text(context.l10n.settingsHome), - subtitle: Text(settings.homePage.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.homePage, - options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsHome, - ), - ); - if (value != null) { - settings.homePage = value; - } - }, - ), - ListTile( - title: Text(context.l10n.settingsKeepScreenOnTile), - subtitle: Text(settings.keepScreenOn.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.keepScreenOn, - options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsKeepScreenOnTitle, - ), - ); - if (value != null) { - settings.keepScreenOn = value; - } - }, - ), - SwitchListTile( - value: settings.mustBackTwiceToExit, - onChanged: (v) => settings.mustBackTwiceToExit = v, - title: Text(context.l10n.settingsDoubleBackExit), - ), - ], - ); - } - - Widget _buildThumbnailsSection(BuildContext context) { - final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor; - double opacityFor(bool enabled) => enabled ? 1 : .2; - return AvesExpansionTile( - leading: _buildLeading(AIcons.grid, stringToColor('Thumbnails')), - title: context.l10n.settingsSectionThumbnails, - expandedNotifier: _expandedNotifier, - showHighlight: false, - children: [ - SwitchListTile( - value: settings.showThumbnailLocation, - onChanged: (v) => settings.showThumbnailLocation = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)), - AnimatedOpacity( - opacity: opacityFor(settings.showThumbnailLocation), - duration: Durations.toggleableTransitionAnimation, - child: Icon( - AIcons.location, - size: iconSize, - ), - ), - ], - ), - ), - SwitchListTile( - value: settings.showThumbnailRaw, - onChanged: (v) => settings.showThumbnailRaw = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)), - AnimatedOpacity( - opacity: opacityFor(settings.showThumbnailRaw), - duration: Durations.toggleableTransitionAnimation, - child: Icon( - AIcons.raw, - size: iconSize, - ), - ), - ], - ), - ), - SwitchListTile( - value: settings.showThumbnailVideoDuration, - onChanged: (v) => settings.showThumbnailVideoDuration = v, - title: Text(context.l10n.settingsThumbnailShowVideoDuration), - ), - ], - ); - } - - Widget _buildViewerSection(BuildContext context) { - return AvesExpansionTile( - leading: _buildLeading(AIcons.image, stringToColor('Image')), - title: context.l10n.settingsSectionViewer, - expandedNotifier: _expandedNotifier, - showHighlight: false, - children: [ - ViewerActionsTile(), - SwitchListTile( - value: settings.showOverlayMinimap, - onChanged: (v) => settings.showOverlayMinimap = v, - title: Text(context.l10n.settingsViewerShowMinimap), - ), - SwitchListTile( - value: settings.showOverlayInfo, - onChanged: (v) => settings.showOverlayInfo = v, - title: Text(context.l10n.settingsViewerShowInformation), - subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle), - ), - SwitchListTile( - value: settings.showOverlayShootingDetails, - onChanged: settings.showOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null, - title: Text(context.l10n.settingsViewerShowShootingDetails), - ), - ListTile( - title: Text(context.l10n.settingsRasterImageBackground), - trailing: EntryBackgroundSelector( - getter: () => settings.rasterBackground, - setter: (value) => settings.rasterBackground = value, - ), - ), - ListTile( - title: Text(context.l10n.settingsVectorImageBackground), - trailing: EntryBackgroundSelector( - getter: () => settings.vectorBackground, - setter: (value) => settings.vectorBackground = value, - ), - ), - ], - ); - } - - Widget _buildVideoSection(BuildContext context) { - final hiddenFilters = settings.hiddenFilters; - final showVideos = !hiddenFilters.contains(MimeFilter.video); - return AvesExpansionTile( - leading: _buildLeading(AIcons.video, stringToColor('Video')), - title: context.l10n.settingsSectionVideo, - expandedNotifier: _expandedNotifier, - showHighlight: false, - children: [ - SwitchListTile( - value: showVideos, - onChanged: (v) => context.read().changeFilterVisibility(MimeFilter.video, v), - title: Text(context.l10n.settingsVideoShowVideos), - ), - SwitchListTile( - value: settings.enableVideoHardwareAcceleration, - onChanged: (v) => settings.enableVideoHardwareAcceleration = v, - title: Text(context.l10n.settingsVideoEnableHardwareAcceleration), - ), - SwitchListTile( - value: settings.enableVideoAutoPlay, - onChanged: (v) => settings.enableVideoAutoPlay = v, - title: Text(context.l10n.settingsVideoEnableAutoPlay), - ), - ListTile( - title: Text(context.l10n.settingsVideoLoopModeTile), - subtitle: Text(settings.videoLoopMode.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.videoLoopMode, - options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsVideoLoopModeTitle, - ), - ); - if (value != null) { - settings.videoLoopMode = value; - } - }, - ), - VideoActionsTile(), - ], - ); - } - - Widget _buildPrivacySection(BuildContext context) { - return AvesExpansionTile( - leading: _buildLeading(AIcons.privacy, stringToColor('Privacy')), - title: context.l10n.settingsSectionPrivacy, - expandedNotifier: _expandedNotifier, - showHighlight: false, - children: [ - SwitchListTile( - value: settings.isCrashlyticsEnabled, - onChanged: (v) => settings.isCrashlyticsEnabled = v, - title: Text(context.l10n.settingsEnableAnalytics), - ), - SwitchListTile( - value: settings.saveSearchHistory, - onChanged: (v) { - settings.saveSearchHistory = v; - if (!v) { - settings.searchHistory = []; - } - }, - title: Text(context.l10n.settingsSaveSearchHistory), - ), - HiddenFilterTile(), - StorageAccessTile(), - ], - ); - } - - Widget _buildLanguageSection(BuildContext context) { - return AvesExpansionTile( - // 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: _buildLeading(AIcons.language, stringToColor('Language')), - title: context.l10n.settingsSectionLanguage, - expandedNotifier: _expandedNotifier, - showHighlight: false, - children: [ - LanguageTile(), - ListTile( - title: Text(context.l10n.settingsCoordinateFormatTile), - subtitle: Text(settings.coordinateFormat.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.coordinateFormat, - options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))), - optionSubtitleBuilder: (value) => value.format(Constants.pointNemo), - title: context.l10n.settingsCoordinateFormatTitle, - ), - ); - if (value != null) { - settings.coordinateFormat = value; - } - }, - ), - ], - ); - } - - Widget _buildLeading(IconData icon, Color color) => Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - border: Border.fromBorderSide(BorderSide( - color: color, - width: AvesFilterChip.outlineWidth, - )), - shape: BoxShape.circle, - ), - child: DecoratedIcon( - icon, - shadows: Constants.embossShadows, - size: 18, - ), - ); } diff --git a/lib/widgets/settings/thumbnails.dart b/lib/widgets/settings/thumbnails.dart new file mode 100644 index 000000000..a91f1a2ba --- /dev/null +++ b/lib/widgets/settings/thumbnails.dart @@ -0,0 +1,79 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/color_utils.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: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); + + @override + Widget build(BuildContext context) { + final currentShowThumbnailLocation = context.select((s) => s.showThumbnailLocation); + final currentShowThumbnailRaw = context.select((s) => s.showThumbnailRaw); + final currentShowThumbnailVideoDuration = context.select((s) => s.showThumbnailVideoDuration); + + final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor; + double opacityFor(bool enabled) => enabled ? 1 : .2; + + return AvesExpansionTile( + leading: SettingsTileLeading( + icon: AIcons.grid, + color: stringToColor('Thumbnails'), + ), + title: context.l10n.settingsSectionThumbnails, + expandedNotifier: expandedNotifier, + showHighlight: false, + children: [ + SwitchListTile( + value: currentShowThumbnailLocation, + onChanged: (v) => settings.showThumbnailLocation = v, + title: Row( + children: [ + Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)), + AnimatedOpacity( + opacity: opacityFor(currentShowThumbnailLocation), + duration: Durations.toggleableTransitionAnimation, + child: Icon( + AIcons.location, + size: iconSize, + ), + ), + ], + ), + ), + SwitchListTile( + value: currentShowThumbnailRaw, + onChanged: (v) => settings.showThumbnailRaw = v, + title: Row( + children: [ + Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)), + AnimatedOpacity( + opacity: opacityFor(currentShowThumbnailRaw), + duration: Durations.toggleableTransitionAnimation, + child: Icon( + AIcons.raw, + size: iconSize, + ), + ), + ], + ), + ), + SwitchListTile( + value: currentShowThumbnailVideoDuration, + onChanged: (v) => settings.showThumbnailVideoDuration = v, + title: Text(context.l10n.settingsThumbnailShowVideoDuration), + ), + ], + ); + } +} diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart new file mode 100644 index 000000000..86b237527 --- /dev/null +++ b/lib/widgets/settings/video/video.dart @@ -0,0 +1,76 @@ +import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/settings/video_loop_mode.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/color_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/video/video_actions_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class VideoSection extends StatelessWidget { + final ValueNotifier expandedNotifier; + + const VideoSection({ + Key? key, + required this.expandedNotifier, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final currentShowVideos = context.select((s) => !s.hiddenFilters.contains(MimeFilter.video)); + final currentEnableVideoHardwareAcceleration = context.select((s) => s.enableVideoHardwareAcceleration); + final currentEnableVideoAutoPlay = context.select((s) => s.enableVideoAutoPlay); + final currentVideoLoopMode = context.select((s) => s.videoLoopMode); + + return AvesExpansionTile( + leading: SettingsTileLeading( + icon: AIcons.video, + color: stringToColor('Video'), + ), + title: context.l10n.settingsSectionVideo, + expandedNotifier: expandedNotifier, + showHighlight: false, + children: [ + SwitchListTile( + value: currentShowVideos, + onChanged: (v) => context.read().changeFilterVisibility(MimeFilter.video, v), + title: Text(context.l10n.settingsVideoShowVideos), + ), + SwitchListTile( + value: currentEnableVideoHardwareAcceleration, + onChanged: (v) => settings.enableVideoHardwareAcceleration = v, + title: Text(context.l10n.settingsVideoEnableHardwareAcceleration), + ), + SwitchListTile( + value: currentEnableVideoAutoPlay, + onChanged: (v) => settings.enableVideoAutoPlay = v, + title: Text(context.l10n.settingsVideoEnableAutoPlay), + ), + ListTile( + title: Text(context.l10n.settingsVideoLoopModeTile), + subtitle: Text(currentVideoLoopMode.getName(context)), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentVideoLoopMode, + options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsVideoLoopModeTitle, + ), + ); + if (value != null) { + settings.videoLoopMode = value; + } + }, + ), + VideoActionsTile(), + ], + ); + } +} diff --git a/lib/widgets/settings/video_actions_editor.dart b/lib/widgets/settings/video/video_actions_editor.dart similarity index 94% rename from lib/widgets/settings/video_actions_editor.dart rename to lib/widgets/settings/video/video_actions_editor.dart index 02b5471ea..b0b5763c1 100644 --- a/lib/widgets/settings/video_actions_editor.dart +++ b/lib/widgets/settings/video/video_actions_editor.dart @@ -1,7 +1,7 @@ import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/settings/quick_actions/editor_page.dart'; +import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; class VideoActionsTile extends StatelessWidget { diff --git a/lib/widgets/settings/entry_background.dart b/lib/widgets/settings/viewer/entry_background.dart similarity index 100% rename from lib/widgets/settings/entry_background.dart rename to lib/widgets/settings/viewer/entry_background.dart diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart new file mode 100644 index 000000000..cb03db693 --- /dev/null +++ b/lib/widgets/settings/viewer/viewer.dart @@ -0,0 +1,72 @@ +import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/color_utils.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/viewer/entry_background.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); + + @override + Widget build(BuildContext context) { + final currentShowOverlayMinimap = context.select((s) => s.showOverlayMinimap); + final currentShowOverlayInfo = context.select((s) => s.showOverlayInfo); + final currentShowOverlayShootingDetails = context.select((s) => s.showOverlayShootingDetails); + final currentRasterBackground = context.select((s) => s.rasterBackground); + final currentVectorBackground = context.select((s) => s.vectorBackground); + + return AvesExpansionTile( + leading: SettingsTileLeading( + icon: AIcons.image, + color: stringToColor('Image'), + ), + title: context.l10n.settingsSectionViewer, + expandedNotifier: expandedNotifier, + showHighlight: false, + children: [ + ViewerActionsTile(), + SwitchListTile( + value: currentShowOverlayMinimap, + onChanged: (v) => settings.showOverlayMinimap = v, + title: Text(context.l10n.settingsViewerShowMinimap), + ), + SwitchListTile( + value: currentShowOverlayInfo, + onChanged: (v) => settings.showOverlayInfo = v, + title: Text(context.l10n.settingsViewerShowInformation), + subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle), + ), + SwitchListTile( + value: currentShowOverlayShootingDetails, + onChanged: currentShowOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null, + title: Text(context.l10n.settingsViewerShowShootingDetails), + ), + ListTile( + title: Text(context.l10n.settingsRasterImageBackground), + trailing: EntryBackgroundSelector( + getter: () => currentRasterBackground, + setter: (value) => settings.rasterBackground = value, + ), + ), + ListTile( + title: Text(context.l10n.settingsVectorImageBackground), + trailing: EntryBackgroundSelector( + getter: () => currentVectorBackground, + setter: (value) => settings.vectorBackground = value, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/settings/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart similarity index 95% rename from lib/widgets/settings/viewer_actions_editor.dart rename to lib/widgets/settings/viewer/viewer_actions_editor.dart index 630d0a37d..7793f661f 100644 --- a/lib/widgets/settings/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -1,7 +1,7 @@ import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/settings/quick_actions/editor_page.dart'; +import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; class ViewerActionsTile extends StatelessWidget { diff --git a/lib/widgets/viewer/visual/subtitle/ass_parser.dart b/lib/widgets/viewer/visual/subtitle/ass_parser.dart index 1b87137a3..5ab2bd893 100644 --- a/lib/widgets/viewer/visual/subtitle/ass_parser.dart +++ b/lib/widgets/viewer/visual/subtitle/ass_parser.dart @@ -40,7 +40,7 @@ class AssParser { static const noBreakSpace = '\u00A0'; - // Parse text with ASS format tags + // Parse text with ASS style overrides // cf https://aegi.vmoe.info/docs/3.0/ASS_Tags/ // e.g. `And I'm like, "We can't {\i1}not{\i0} see it."` // e.g. `{\fad(200,200)\blur3}lorem ipsum"` diff --git a/lib/widgets/viewer/visual/subtitle/subtitle.dart b/lib/widgets/viewer/visual/subtitle/subtitle.dart index 755ee1180..ee6c74d99 100644 --- a/lib/widgets/viewer/visual/subtitle/subtitle.dart +++ b/lib/widgets/viewer/visual/subtitle/subtitle.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/basic/outlined_text.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; @@ -15,16 +16,13 @@ class VideoSubtitles extends StatelessWidget { final ValueNotifier viewStateNotifier; final bool debugMode; - static const baseStyle = TextStyle( - color: Colors.white, - fontSize: 20, - shadows: [ - Shadow( - color: Colors.black54, - offset: Offset(1, 1), - ), - ], - ); + static const baseOutlineColor = Colors.black; + static const baseShadows = [ + Shadow( + color: Colors.black54, + offset: Offset(1, 1), + ), + ]; const VideoSubtitles({ Key? key, @@ -37,209 +35,223 @@ class VideoSubtitles extends StatelessWidget { Widget build(BuildContext context) { final videoDisplaySize = controller.entry.videoDisplaySize(controller.sarNotifier.value); return IgnorePointer( - child: Selector( - selector: (c, mq) => mq.orientation, - builder: (c, orientation, child) { - final bottom = orientation == Orientation.portrait ? .5 : .8; - Alignment toVerticalAlignment(SubtitleStyle extraStyle) { - switch (extraStyle.vAlign) { - case TextAlignVertical.top: - return Alignment(0, -bottom); - case TextAlignVertical.center: - return Alignment.center; - case TextAlignVertical.bottom: - default: - return Alignment(0, bottom); - } - } + child: Consumer( + builder: (context, settings, child) { + final baseTextAlign = settings.subtitleTextAlignment; + final baseOutlineWidth = settings.subtitleShowOutline ? 1 : 0; + final baseStyle = TextStyle( + color: settings.subtitleTextColor, + backgroundColor: settings.subtitleBackgroundColor, + fontSize: settings.subtitleFontSize, + shadows: settings.subtitleShowOutline ? baseShadows : null, + ); - final viewportSize = context.read().size; + return Selector( + selector: (c, mq) => mq.orientation, + builder: (c, orientation, child) { + final bottom = orientation == Orientation.portrait ? .5 : .8; + Alignment toVerticalAlignment(SubtitleStyle extraStyle) { + switch (extraStyle.vAlign) { + case TextAlignVertical.top: + return Alignment(0, -bottom); + case TextAlignVertical.center: + return Alignment.center; + case TextAlignVertical.bottom: + default: + return Alignment(0, bottom); + } + } - return ValueListenableBuilder( - valueListenable: viewStateNotifier, - builder: (context, viewState, child) { - final viewPosition = viewState.position; - final viewScale = viewState.scale ?? 1; - final viewSize = videoDisplaySize * viewScale; - final viewOffset = Offset( - (viewportSize.width - viewSize.width) / 2, - (viewportSize.height - viewSize.height) / 2, - ); + final viewportSize = context.read().size; - return StreamBuilder( - stream: controller.timedTextStream, - builder: (context, snapshot) { - final text = snapshot.data; - if (text == null) return const SizedBox(); + return ValueListenableBuilder( + valueListenable: viewStateNotifier, + builder: (context, viewState, child) { + final viewPosition = viewState.position; + final viewScale = viewState.scale ?? 1; + final viewSize = videoDisplaySize * viewScale; + final viewOffset = Offset( + (viewportSize.width - viewSize.width) / 2, + (viewportSize.height - viewSize.height) / 2, + ); - if (debugMode) { - return Padding( - padding: const EdgeInsets.only(top: 100.0), - child: Align( - alignment: Alignment.topLeft, - child: OutlinedText( - textSpans: [ - TextSpan( - text: text, - style: const TextStyle(fontSize: 14), - ) - ], - outlineWidth: 1, - outlineColor: Colors.black, - ), - ), - ); - } + return StreamBuilder( + stream: controller.timedTextStream, + builder: (context, snapshot) { + final text = snapshot.data; + if (text == null) return const SizedBox(); - final styledLine = AssParser.parse(text, baseStyle, viewScale); - final position = styledLine.position; - final clip = styledLine.clip; - final styledSpans = styledLine.spans; - final byExtraStyle = groupBy(styledSpans, (v) => v.extraStyle); - return Stack( - children: byExtraStyle.entries.map((kv) { - final extraStyle = kv.key; - final spans = kv.value.map((v) { - final span = v.textSpan; - final style = span.style; - if (position == null || style == null) return span; - - final letterSpacing = style.letterSpacing; - final shadows = style.shadows; - return TextSpan( - text: span.text, - style: style.copyWith( - letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null, - shadows: shadows != null - ? shadows - .map((v) => Shadow( - color: v.color, - offset: v.offset * viewScale, - blurRadius: v.blurRadius * viewScale, - )) - .toList() - : null, + if (debugMode) { + return Padding( + padding: const EdgeInsets.only(top: 100.0), + child: Align( + alignment: Alignment.topLeft, + child: OutlinedText( + textSpans: [ + TextSpan( + text: text, + style: const TextStyle(fontSize: 14), + ) + ], + outlineWidth: 1, + outlineColor: Colors.black, + ), ), ); - }).toList(); - final drawingPaths = extraStyle.drawingPaths; - - Widget child; - if (drawingPaths != null) { - child = CustomPaint( - painter: SubtitlePathPainter( - paths: drawingPaths, - scale: viewScale, - fillColor: spans.firstOrNull?.style?.color ?? Colors.white, - strokeColor: extraStyle.borderColor, - ), - ); - } else { - final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1); - child = OutlinedText( - textSpans: spans, - outlineWidth: outlineWidth * (position != null ? viewScale : 1), - outlineColor: extraStyle.borderColor ?? Colors.black, - outlineBlurSigma: extraStyle.edgeBlur ?? 0, - textAlign: extraStyle.hAlign ?? TextAlign.center, - ); } - var transform = Matrix4.identity(); + final styledLine = AssParser.parse(text, baseStyle, viewScale); + final position = styledLine.position; + final clip = styledLine.clip; + final styledSpans = styledLine.spans; + final byExtraStyle = groupBy(styledSpans, (v) => v.extraStyle); + return Stack( + children: byExtraStyle.entries.map((kv) { + final extraStyle = kv.key; + final spans = kv.value.map((v) { + final span = v.textSpan; + final style = span.style; + if (position == null || style == null) return span; - if (position != null) { - final para = RenderParagraph( - TextSpan(children: spans), - textDirection: TextDirection.ltr, - textScaleFactor: context.read().textScaleFactor, - )..layout(const BoxConstraints()); - final textWidth = para.getMaxIntrinsicWidth(double.infinity); - final textHeight = para.getMaxIntrinsicHeight(double.infinity); + final letterSpacing = style.letterSpacing; + final shadows = style.shadows; + return TextSpan( + text: span.text, + style: style.copyWith( + letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null, + shadows: shadows != null + ? shadows + .map((v) => Shadow( + color: v.color, + offset: v.offset * viewScale, + blurRadius: v.blurRadius * viewScale, + )) + .toList() + : null, + ), + ); + }).toList(); + final drawingPaths = extraStyle.drawingPaths; - late double anchorOffsetX, anchorOffsetY; - switch (extraStyle.hAlign) { - case TextAlign.left: - anchorOffsetX = 0; - break; - case TextAlign.right: - anchorOffsetX = -textWidth; - break; - case TextAlign.center: - default: - anchorOffsetX = -textWidth / 2; - break; - } - switch (extraStyle.vAlign) { - case TextAlignVertical.top: - anchorOffsetY = 0; - break; - case TextAlignVertical.center: - anchorOffsetY = -textHeight / 2; - break; - case TextAlignVertical.bottom: - default: - anchorOffsetY = -textHeight; - break; - } - final alignOffset = Offset(anchorOffsetX, anchorOffsetY); - final lineOffset = position * viewScale + viewPosition; - final translateOffset = viewOffset + lineOffset + alignOffset; - transform.translate(translateOffset.dx, translateOffset.dy); - } + Widget child; + if (drawingPaths != null) { + child = CustomPaint( + painter: SubtitlePathPainter( + paths: drawingPaths, + scale: viewScale, + fillColor: spans.firstOrNull?.style?.color ?? Colors.white, + strokeColor: extraStyle.borderColor, + ), + ); + } else { + final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1); + child = OutlinedText( + textSpans: spans, + outlineWidth: outlineWidth * (position != null ? viewScale : baseOutlineWidth), + outlineColor: extraStyle.borderColor ?? baseOutlineColor, + outlineBlurSigma: extraStyle.edgeBlur ?? 0, + textAlign: extraStyle.hAlign ?? baseTextAlign, + ); + } - if (extraStyle.rotating) { - // for perspective - transform.setEntry(3, 2, 0.001); - final x = -toRadians(extraStyle.rotationX ?? 0); - final y = -toRadians(extraStyle.rotationY ?? 0); - final z = -toRadians(extraStyle.rotationZ ?? 0); - if (x != 0) transform.rotateX(x); - if (y != 0) transform.rotateY(y); - if (z != 0) transform.rotateZ(z); - } - if (extraStyle.scaling) { - final x = extraStyle.scaleX ?? 1; - final y = extraStyle.scaleY ?? 1; - transform.scale(x, y); - } - if (extraStyle.shearing) { - final x = extraStyle.shearX ?? 0; - final y = extraStyle.shearY ?? 0; - transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)); - } + var transform = Matrix4.identity(); - if (!transform.isIdentity()) { - child = Transform( - transform: transform, - alignment: Alignment.center, - child: child, - ); - } + if (position != null) { + final para = RenderParagraph( + TextSpan(children: spans), + textDirection: TextDirection.ltr, + textScaleFactor: context.read().textScaleFactor, + )..layout(const BoxConstraints()); + final textWidth = para.getMaxIntrinsicWidth(double.infinity); + final textHeight = para.getMaxIntrinsicHeight(double.infinity); - if (position == null) { - child = Align( - alignment: toVerticalAlignment(extraStyle), - child: child, - ); - } + late double anchorOffsetX, anchorOffsetY; + switch (extraStyle.hAlign ?? baseTextAlign) { + case TextAlign.left: + anchorOffsetX = 0; + break; + case TextAlign.right: + anchorOffsetX = -textWidth; + break; + case TextAlign.center: + case TextAlign.start: + case TextAlign.end: + case TextAlign.justify: + anchorOffsetX = -textWidth / 2; + break; + } + switch (extraStyle.vAlign ?? TextAlignVertical.bottom) { + case TextAlignVertical.top: + anchorOffsetY = 0; + break; + case TextAlignVertical.center: + anchorOffsetY = -textHeight / 2; + break; + case TextAlignVertical.bottom: + anchorOffsetY = -textHeight; + break; + } + final alignOffset = Offset(anchorOffsetX, anchorOffsetY); + final lineOffset = position * viewScale + viewPosition; + final translateOffset = viewOffset + lineOffset + alignOffset; + transform.translate(translateOffset.dx, translateOffset.dy); + } - if (clip != null) { - final clipOffset = viewOffset + viewPosition; - final matrix = Matrix4.identity() - ..translate(clipOffset.dx, clipOffset.dy) - ..scale(viewScale, viewScale); - final transform = matrix.storage; - child = ClipPath( - clipper: SubtitlePathClipper( - paths: clip.map((v) => v.transform(transform)).toList(), - scale: viewScale, - ), - child: child, - ); - } + if (extraStyle.rotating) { + // for perspective + transform.setEntry(3, 2, 0.001); + final x = -toRadians(extraStyle.rotationX ?? 0); + final y = -toRadians(extraStyle.rotationY ?? 0); + final z = -toRadians(extraStyle.rotationZ ?? 0); + if (x != 0) transform.rotateX(x); + if (y != 0) transform.rotateY(y); + if (z != 0) transform.rotateZ(z); + } + if (extraStyle.scaling) { + final x = extraStyle.scaleX ?? 1; + final y = extraStyle.scaleY ?? 1; + transform.scale(x, y); + } + if (extraStyle.shearing) { + final x = extraStyle.shearX ?? 0; + final y = extraStyle.shearY ?? 0; + transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)); + } - return child; - }).toList(), + if (!transform.isIdentity()) { + child = Transform( + transform: transform, + alignment: Alignment.center, + child: child, + ); + } + + if (position == null) { + child = Align( + alignment: toVerticalAlignment(extraStyle), + child: child, + ); + } + + if (clip != null) { + final clipOffset = viewOffset + viewPosition; + final matrix = Matrix4.identity() + ..translate(clipOffset.dx, clipOffset.dy) + ..scale(viewScale, viewScale); + final transform = matrix.storage; + child = ClipPath( + clipper: SubtitlePathClipper( + paths: clip.map((v) => v.transform(transform)).toList(), + scale: viewScale, + ), + child: child, + ); + } + + return child; + }).toList(), + ); + }, ); }, );