From 3e4c2007761979b57cd5c6be0dc83606b9b16e1c Mon Sep 17 00:00:00 2001 From: SmartDevStar Date: Tue, 18 Jul 2023 06:33:38 -0600 Subject: [PATCH 1/7] added subtitle font settings in subtitle option dialog and added a toggle button to show media player bottom bar permanently --- yuuna/lib/i18n/strings.g.dart | 2294 ++++++++++------- yuuna/lib/src/models/app_model.dart | 9 + .../implementations/player_source_page.dart | 128 +- .../subtitle_options_dialog_page.dart | 183 +- .../src/utils/player/subtitle_options.dart | 12 + 5 files changed, 1710 insertions(+), 916 deletions(-) diff --git a/yuuna/lib/i18n/strings.g.dart b/yuuna/lib/i18n/strings.g.dart index 720cc17b..305c0725 100644 --- a/yuuna/lib/i18n/strings.g.dart +++ b/yuuna/lib/i18n/strings.g.dart @@ -22,17 +22,25 @@ const AppLocale _baseLocale = AppLocale.en; /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: _StringsEn.build); + en(languageCode: 'en', build: _StringsEn.build); - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + const AppLocale( + {required this.languageCode, + this.scriptCode, + this.countryCode, + required this.build}); // ignore: unused_element - @override final String languageCode; - @override final String? scriptCode; - @override final String? countryCode; - @override final TranslationBuilder build; + @override + final String languageCode; + @override + final String? scriptCode; + @override + final String? countryCode; + @override + final TranslationBuilder build; - /// Gets current instance managed by [LocaleSettings]. - _StringsEn get translations => LocaleSettings.instance.translationMap[this]!; + /// Gets current instance managed by [LocaleSettings]. + _StringsEn get translations => LocaleSettings.instance.translationMap[this]!; } /// Method A: Simple @@ -62,16 +70,20 @@ _StringsEn get t => LocaleSettings.instance.currentTranslations; /// String a = t.someKey.anotherKey; // Use t variable. /// String b = t['someKey.anotherKey']; // Only for edge cases! class Translations { - Translations._(); // no constructor + Translations._(); // no constructor - static _StringsEn of(BuildContext context) => InheritedLocaleData.of(context).translations; + static _StringsEn of(BuildContext context) => + InheritedLocaleData.of(context).translations; } /// The provider for method B -class TranslationProvider extends BaseTranslationProvider { - TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); +class TranslationProvider + extends BaseTranslationProvider { + TranslationProvider({required super.child}) + : super(settings: LocaleSettings.instance); - static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); + static InheritedLocaleData of(BuildContext context) => + InheritedLocaleData.of(context); } /// Method B shorthand via [BuildContext] extension method. @@ -80,899 +92,1435 @@ class TranslationProvider extends BaseTranslationProvider /// Usage (e.g. in a widget's build method): /// context.t.someKey.anotherKey extension BuildContextTranslationsExtension on BuildContext { - _StringsEn get t => TranslationProvider.of(this).translations; + _StringsEn get t => TranslationProvider.of(this).translations; } /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); - - static final instance = LocaleSettings._(); - - // static aliases (checkout base methods for documentation) - static AppLocale get currentLocale => instance.currentLocale; - static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( - language: language, - locale: locale, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); + LocaleSettings._() : super(utils: AppLocaleUtils.instance); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static AppLocale setLocale(AppLocale locale, + {bool? listenToDeviceLocale = false}) => + instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRaw(String rawLocale, + {bool? listenToDeviceLocale = false}) => + instance.setLocaleRaw(rawLocale, + listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocale() => instance.useDeviceLocale(); + @Deprecated('Use [AppLocaleUtils.supportedLocales]') + static List get supportedLocales => instance.supportedLocales; + @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') + static List get supportedLocalesRaw => instance.supportedLocalesRaw; + static void setPluralResolver( + {String? language, + AppLocale? locale, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) => + instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); } /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + AppLocaleUtils._() + : super(baseLocale: _baseLocale, locales: AppLocale.values); - static final instance = AppLocaleUtils._(); + static final instance = AppLocaleUtils._(); - // static aliases (checkout base methods for documentation) - static AppLocale parse(String rawLocale) => instance.parse(rawLocale); - static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); - static AppLocale findDeviceLocale() => instance.findDeviceLocale(); - static List get supportedLocales => instance.supportedLocales; - static List get supportedLocalesRaw => instance.supportedLocalesRaw; + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts( + {required String languageCode, + String? scriptCode, + String? countryCode}) => + instance.parseLocaleParts( + languageCode: languageCode, + scriptCode: scriptCode, + countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; } // translations // Path: class _StringsEn implements BaseTranslations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsEn.build( + {Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) + : assert(overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; + + /// Access flat map + dynamic operator [](String key) => $meta.getTranslation(key); + + late final _StringsEn _root = this; // ignore: unused_field - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsEn.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - dynamic operator[](String key) => $meta.getTranslation(key); - - late final _StringsEn _root = this; // ignore: unused_field - - // Translations - String get dictionary_media_type => 'Dictionary'; - String get player_media_type => 'Player'; - String get reader_media_type => 'Reader'; - String get viewer_media_type => 'Viewer'; - String get back => 'Back'; - String get search => 'Search'; - String get search_ellipsis => 'Search...'; - String get show_more => 'Show More'; - String get show_menu => 'Show Menu'; - String get stash => 'Stash'; - String get pick_image => 'Pick Image'; - String get undo => 'Undo'; - String get copy => 'Copy'; - String get clear => 'Clear'; - String get creator => 'Creator'; - String get share => 'Share'; - String get resume_last_media => 'Resume Last Media'; - String get change_source => 'Change Source'; - String get launch_source => 'Launch Source'; - String get card_creator => 'Card Creator'; - String get target_language => 'Target language'; - String get show_options => 'Show Options'; - String get switch_profiles => 'Switch Profiles'; - String get dictionaries => 'Dictionaries'; - String get enhancements => 'Enhancements'; - String get app_locale => 'App locale'; - String get app_locale_warning => 'Community addons and enhancements are managed by their respective developers, and these may appear in their original language.'; - String get dialog_play => 'PLAY'; - String get dialog_read => 'READ'; - String get dialog_view => 'VIEW'; - String get dialog_edit => 'EDIT'; - String get dialog_export => 'EXPORT'; - String get dialog_import => 'IMPORT'; - String get dialog_close => 'CLOSE'; - String get dialog_clear => 'CLEAR'; - String get dialog_create => 'CREATE'; - String get dialog_delete => 'DELETE'; - String get dialog_cancel => 'CANCEL'; - String get dialog_select => 'SELECT'; - String get dialog_stash => 'STASH'; - String get dialog_search => 'SEARCH'; - String get dialog_exit => 'EXIT'; - String get dialog_share => 'SHARE'; - String get dialog_pop => 'POP'; - String get dialog_save => 'SAVE'; - String get dialog_set => 'SET'; - String get dialog_browse => 'BROWSE'; - String get dialog_channel => 'CHANNEL'; - String get dialog_directory => 'DIRECTORY'; - String get dialog_crop => 'CROP'; - String get dialog_connect => 'CONNECT'; - String get dialog_append => 'APPEND'; - String get dialog_record => 'RECORD'; - String get dialog_manage => 'MANAGE'; - String get dialog_stop => 'STOP'; - String get dialog_done => 'DONE'; - String get reset => 'Reset'; - String get dialog_launch_ankidroid => 'LAUNCH ANKIDROID'; - String get media_item_delete_confirmation => 'This will clear this item from history. Are you sure you want to do this?'; - String get dictionaries_delete_confirmation => 'Deleting a dictionary will also clear all dictionary results from history. Are you sure you want to do this?'; - String get mappings_delete_confirmation => 'This profile will be deleted. Are you sure you want to do this?'; - String get catalog_delete_confirmation => 'This catalog will be deleted. Are you sure you want to do this?'; - String get dictionaries_deleting_data => 'Deleting dictionary data...'; - String get dictionaries_menu_empty => 'Import a dictionary for use'; - String get options_theme_light => 'Use light theme'; - String get options_theme_dark => 'Use dark theme'; - String get options_incognito_on => 'Turn on incognito mode'; - String get options_incognito_off => 'Turn off incognito mode'; - String get options_dictionaries => 'Manage dictionaries'; - String get options_profiles => 'Export profiles'; - String get options_enhancements => 'User enhancements'; - String get options_language => 'Language settings'; - String get options_github => 'View repository on GitHub'; - String get options_attribution => 'Licenses and attribution'; - String get options_copy => 'Copy'; - String get options_collapse => 'Collapse'; - String get options_expand => 'Expand'; - String get options_delete => 'Delete'; - String get options_show => 'Show'; - String get options_hide => 'Hide'; - String get options_edit => 'Edit'; - String get info_empty_home_tab => 'History is empty'; - String get delete_in_progress => 'Delete in progress'; - String get import_format => 'Import format'; - String get import_in_progress => 'Import in progress'; - String get import_start => 'Preparing for import...'; - String get import_clean => 'Cleaning working space...'; - String get import_extract => 'Extracting files...'; - String import_name({required Object name}) => 'Importing 『${name}』...'; - String get import_entries => 'Processing entries...'; - String import_found_entry({required Object count}) => 'Found ${count} entries...'; - String import_found_tag({required Object count}) => 'Found ${count} tags...'; - String import_found_frequency({required Object count}) => 'Found ${count} frequency entries...'; - String import_found_pitch({required Object count}) => 'Found ${count} pitch accent entries...'; - String import_write_entry({required Object count, required Object total}) => 'Writing entries:\n${count} / ${total}'; - String import_write_tag({required Object count, required Object total}) => 'Writing tags:\n${count} / ${total}'; - String import_write_frequency({required Object count, required Object total}) => 'Writing frequency entries:\n${count} / ${total}'; - String import_write_pitch({required Object count, required Object total}) => 'Writing pitch accent entries:\n${count} / ${total}'; - String get import_failed => 'Dictionary import failed.'; - String get import_complete => 'Dictionary import complete.'; - String import_duplicate({required Object name}) => 'A dictionary with the name『${name}』is already imported.'; - String get dialog_title_dictionary_clear => 'Clear all dictionaries?'; - String get dialog_content_dictionary_clear => 'Wiping the dictionary database will also clear all search results in history.'; - String dialog_title_dictionary_delete({required Object name}) => 'Delete 『${name}』?'; - String get dialog_content_dictionary_delete => 'Deleting a single dictionary may take longer than clearing the entire dictionary database. This will also clear all search results in history.'; - String get delete_dictionary_data => 'Clearing all dictionary data...'; - String dictionary_tag({required Object name}) => 'Imported from ${name}'; - String get legalese => 'A full-featured immersion language learning suite for mobile.\n\nOriginally built for the Japanese language learning community by Leo Rafael Orpilla. Logo by suzy and Aaron Marbella.\n\njidoujisho is free and open source software. See the project repository for a comprehensive list of other licenses and attribution notices. Enjoying the application? Help out by providing feedback, making a donation, reporting issues or contributing improvements on GitHub.'; - String get same_name_dictionary_found => 'Dictionary with same name found.'; - String import_file_extension_invalid({required Object extensions}) => 'This format expects files with the following extensions: ${extensions}'; - String get field_label_empty => 'Empty'; - String get model_to_map => 'Card type to use for new profile'; - String get mapping_name => 'Profile name'; - String get mapping_name_hint => 'Name to assign to profile'; - String get error_profile_name => 'Invalid profile name'; - String get error_profile_name_content => 'A profile with this name already exists or is not valid and cannot be saved.'; - String get error_standard_profile_name => 'Invalid profile name'; - String get error_standard_profile_name_content => 'Cannot rename the standard profile.'; - String get error_ankidroid_api => 'AnkiDroid error'; - String get error_ankidroid_api_content => 'There was an issue communicating with AnkiDroid.\n\nEnsure that the AnkiDroid background service is active and all relevant app permissions are granted in order to continue.'; - String get info_standard_model => 'Standard card type added'; - String get info_standard_model_content => '『jidoujisho Kinomoto』 has been added to AnkiDroid as a new card type.\n\nSetups making use of a different card type or field order may be used by adding a new export profile.'; - String get error_model_missing => 'Missing card type'; - String get error_model_missing_content => 'The corresponding card type of the currently selected profile is missing.\n\nThe profile will be deleted, and the standard profile has now been selected in its place.'; - String get error_model_changed => 'Card type changed'; - String get error_model_changed_content => 'The number of fields of the card type corresponding to the selected profile has changed.\n\nThe fields of the currently selected profile have been reset and will require reconfiguration.'; - String get creator_exporting_as => 'Creating card with profile'; - String get creator_exporting_as_fields_editing => 'Editing fields for profile'; - String get creator_exporting_as_enhancements_editing => 'Editing enhancements for profile'; - String get creator_export_card => 'Create Card'; - String get info_enhancements => 'Enhancements enable the automation of field editing prior to card creation. Pick a slot on the right of a field to allow use of an enhancement. Up to five right slots may be utilised for each field. The enhancement in the left slot of a field will be automatically applied in instant card creation or upon launch of the Card Creator.'; - String get info_actions => 'Quick actions allow for instant card creation and other automations to be used on dictionary search results. Actions can be assigned via the slots below. Up to six slots may be utilised.'; - String get no_more_available_enhancements => 'No more available enhancements for this field'; - String get no_more_available_quick_actions => 'No more available quick actions'; - String get assign_auto_enhancement => 'Assign Auto Enhancement'; - String get assign_manual_enhancement => 'Assign Manual Enhancement'; - String get remove_enhancement => 'Remove Enhancement'; - String copy_of_mapping({required Object name}) => 'Copy of ${name}'; - String get enter_search_term => 'Enter a search term...'; - String searching_for({required Object searchTerm}) => 'Searching for 『${searchTerm}』...'; - String get no_search_results => 'No search results found.'; - String get edit_actions => 'Edit Dictionary Quick Actions'; - String get remove_action => 'Remove Action'; - String get assign_action => 'Assign Action'; - String dictionary_import_tag({required Object name}) => 'Imported from ${name}'; - String stash_added_single({required Object term}) => '『${term}』has been added to the Stash.'; - String get stash_added_multiple => 'Multiple items have been added to the Stash.'; - String stash_clear_single({required Object term}) => '『${term}』has been removed from the Stash.'; - String get stash_clear_title => 'Clear Stash'; - String get stash_clear_description => 'All contents will be cleared. Are you sure?'; - String get stash_placeholder => 'No items in the Stash'; - String get stash_nothing_to_pop => 'No items to be popped from the Stash.'; - String get no_sentences_found => 'No sentences found'; - String get failed_online_service => 'Failed to communicate with online service'; - String get search_label_before => 'Show all '; - String get search_label_middle => 'out of '; - String get search_label_after => 'search results found for'; - String get clear_dictionary_title => 'Clear Dictionary Result History'; - String get clear_dictionary_description => 'This will clear all dictionary results from history. Are you sure?'; - String get clear_search_title => 'Clear Search History'; - String get clear_search_description => 'This will clear all search terms for this history. Are you sure?'; - String get clear_creator_title => 'Clear Creator'; - String get clear_creator_description => 'This will clear all fields. Are you sure?'; - String get copied_to_clipboard => 'Copied to clipboard.'; - String get no_text => 'No text.'; - String get info_fields => 'Fields are pre-filled based on the term selected on instant export or prior to opening the Card Creator. In order to include a field for card export, it must be enabled below as well as mapped in the current selected export profile. Enabled fields may also be collapsed below in order to reduce clutter during editing. Use the Clear button on the top-right of the Card Creator in order to wipe these hidden fields quickly when manually editing a card.'; - String get edit_fields => 'Edit and Reorder Fields'; - String get remove_field => 'Remove Field'; - String get add_field => 'Assign Field'; - String get add_field_hint => 'Assign a field to this row'; - String get no_more_available_fields => 'No more available fields'; - String get hidden_fields => 'Additional fields'; - String field_fallback_used({required Object field, required Object secondField}) => 'The ${field} field used ${secondField} as its fallback search term.'; - String get no_text_to_search => 'No text to search.'; - String get image_search_label_before => 'Selecting image '; - String get image_search_label_middle => 'out of '; - String get image_search_label_after => 'found for'; - String get image_search_label_none_middle => 'no image '; - String get image_search_label_none_before => 'Selecting '; - String get preparing_instant_export => 'Preparing card for export...'; - String get processing_in_progress => 'Preparing images'; - String get searching_in_progress => 'Searching for '; - String get audio_unavailable => 'No audio could be found.'; - String get no_audio_enhancements => 'No audio enhancements are assigned.'; - String card_exported({required Object deck}) => 'Card exported to 『${deck}』.'; - String get info_incognito_on => 'Incognito mode on. Dictionary, media and search history will not be tracked.'; - String get info_incognito_off => 'Incognito mode off. Dictionary, media and search history will be tracked.'; - String get exit_media_title => 'Exit Media'; - String get exit_media_description => 'This will return you to the main menu. Are you sure?'; - String get unimplemented_source => 'Unimplemented source'; - String get clear_browser_title => 'Clear Browser Data'; - String get clear_browser_description => 'This will clear all browsing data used in media sources that use web content. Are you sure?'; - String get ttu_no_books_added => 'No books added to ッツ Ebook Reader'; - String get local_media_directory_empty => 'Directory has no folders or video'; - String get pick_video_file => 'Pick Video File'; - String get navigate_up_one_directory_level => 'Navigate Up One Directory Level'; - String get play => 'Play'; - String get pause => 'Pause'; - String get record => 'Record'; - String get stop => 'Stop'; - String get replay => 'Replay'; - String get audio_subtitles => 'Audio/Subtitles'; - String get player_option_shadowing => 'Shadowing Mode'; - String get player_option_change_mode => 'Change Playback Mode'; - String get player_option_listening_comprehension => 'Listening Comprehension Mode'; - String get player_option_drag_to_select => 'Use Drag to Select Subtitle Selection'; - String get player_option_tap_to_select => 'Use Tap to Select Subtitle Selection'; - String get player_option_dictionary_menu => 'Select Active Dictionary Source'; - String get player_option_cast_video => 'Cast to Display Device'; - String get player_option_share_subtitle => 'Share Current Subtitle'; - String get player_option_export => 'Create Card from Context'; - String get player_option_audio => 'Audio'; - String get player_option_subtitle => 'Subtitle'; - String get player_option_subtitle_external => 'External'; - String get player_option_subtitle_none => 'None'; - String get player_option_select_subtitle => 'Select Subtitle Track'; - String get player_option_select_audio => 'Select Audio Track'; - String get player_option_text_filter => 'Use Regular Expression Filter'; - String get player_option_blur_preferences => 'Blur Widget Preferences'; - String get player_option_blur_use => 'Use Blur Widget'; - String get player_option_blur_radius => 'Blur radius'; - String get player_option_blur_options => 'Set Blur Widget Color and Bluriness'; - String get player_option_blur_reset => 'Reset Blur Widget Size and Position'; - String get player_align_subtitle_transcript => 'Align Subtitle with Transcript'; - String get player_option_subtitle_appearance => 'Subtitle Timing and Appearance'; - String get player_option_load_subtitles => 'Load External Subtitles'; - String get player_option_subtitle_delay => 'Subtitle delay'; - String get player_option_audio_allowance => 'Audio allowance'; - String get player_option_font_name => 'Subtitle font name'; - String get player_option_font_size => 'Subtitle font size'; - String get player_option_regex_filter => 'Regular expression filter'; - String get player_option_subtitle_background_opacity => 'Subtitle background opacity'; - String get player_option_subtitle_background_blur_radius => 'Subtitle background blur radius'; - String get player_option_outline_width => 'Subtitle outline width'; - String get player_option_subtitle_always_above_bottom_bar => 'Always show subtitle above bottom bar area'; - String get player_subtitles_transcript_empty => 'Transcript is empty.'; - String get player_prepare_export => 'Preparing card...'; - String get player_change_player_orientation => 'Change Player Orientation'; - String get no_current_media => 'Play or refresh media for lyrics'; - String get lyrics_permission_required => 'Required permission not granted'; - String get no_lyrics_found => 'No lyrics found'; - String get trending => 'Trending'; - String get caption_filter => 'Filter Closed Captions'; - String get captions_query => 'Querying for captions'; - String get captions_target => 'Target language'; - String get captions_app => 'App language'; - String get captions_other => 'Other language'; - String get captions_closed => 'Closed captioning'; - String get captions_auto => 'Automatic captioning'; - String get captions_unavailable => 'No captioning'; - String get captions_error => 'Error while querying captions'; - String get change_quality => 'Change Quality'; - String get closed_captions_query => 'Querying for captions'; - String get closed_captions_target => 'Target language captions'; - String get closed_captions_app => 'App language captions'; - String get closed_captions_other => 'Other language captions'; - String get closed_captions_unavailable => 'No captions'; - String get closed_captions_error => 'Error while querying captions'; - String get stream_url => 'Stream URL'; - String get default_option => 'Default'; - String get paste => 'Paste'; - String get lyrics_title => 'Title'; - String get lyrics_artist => 'Artist'; - String get set_media => 'Set Media'; - String get no_recordings_found => 'No recordings found'; - String get wrap_image_audio => 'Include image/audio HTML tags on export'; - String get server_address => 'Server Address'; - String get no_active_connection => 'No active connection'; - String get failed_server_connection => 'Failed to connect to server'; - String get no_text_received => 'No text received'; - String get text_segmentation => 'Text Segmentation'; - String get connect_disconnect => 'Connect/Disconnect'; - String get clear_text_title => 'Clear Text'; - String get clear_text_description => 'This will clear all received text. Are you sure?'; - String get close_connection_title => 'Close Connection'; - String get close_connection_description => 'This will end the WebSocket connection and clear all received text. Are you sure?'; - String get use_slow_import => 'Slow import (use if failing)'; - String get settings => 'Settings'; - String get manager => 'Manager'; - String get volume_button_page_turning => 'Volume button page turning'; - String get invert_volume_buttons => 'Invert volume buttons'; - String get volume_button_turning_speed => 'Continuous scrolling speed'; - String get extend_page_beyond_navbar => 'Extend page beyond navigation bar'; - String get tweaks => 'Tweaks'; - String get increase => 'Increase'; - String get decrease => 'Decrease'; - String get unit_milliseconds => 'ms'; - String get unit_pixels => 'px'; - String get dictionary_settings => 'Dictionary Settings'; - String get auto_search => 'Auto search'; - String get auto_search_debounce_delay => 'Auto search debounce delay'; - String get dictionary_font_size => 'Dictionary font size'; - String get close_on_export => 'Close on Export'; - String get close_on_export_on => 'The Card Creator will now automatically close upon card export.'; - String get close_on_export_off => 'The Card Creator will no longer close upon card export.'; - String get export_profile_empty => 'Your export profile has no set fields and requires configuration.'; - String get error_export_media_ankidroid => 'There was an error in exporting media to AnkiDroid.'; - String get error_add_note => 'There was an error in adding a note to AnkiDroid.'; - String get first_time_setup => 'First-Time Setup'; - String get first_time_setup_description => 'Welcome to jidoujisho! Set your target language and a default profile will be tailored for you. You can change this later at anytime.'; - String get maximum_entries => 'Maximum dictionary entry query limit'; - String get maximum_terms => 'Maximum dictionary headwords in result'; - String get use_br_tags => 'Use line break tag instead of newline on export'; - String get prepend_dictionary_names => 'Prepend dictionary name in meaning'; - String get highlight_on_tap => 'Highlight text on tap'; - String get no_audio_file => 'No audio file to save.'; - String get storage_permissions => 'Please grant the following permissions for exporting to AnkiDroid.'; - String get stream => 'Stream'; - String get network_subtitles_warning => 'Embedded subtitles are unsupported for network streams.'; - String get accessibility => 'Permission is required to capture text from accessibility events.'; - String get comments => 'Comments'; - String get replies => 'Replies'; - String get no_comments_queried => 'No comments queried'; - String get no_text_in_clipboard => 'No text to display'; - String file_downloaded({required Object name}) => 'File downloaded: ${name}'; - String get cfhange_sort_order => 'Change Sort Order'; - String get login => 'Login'; - String get send => 'Send'; - String get no_messages => 'Start a chat'; - String get enter_message => 'Enter message...'; - String get clear_message_title => 'Clear Messages'; - String get clear_message_description => 'This will clear all messages and start a new chat. Are you sure?'; - String get error_chatgpt_response => 'Request failed or rate-limited. Try again shortly or check your usage limits.'; - String get pick_file => 'Pick File'; - String get open_url => 'Open URL'; - String get catalogs => 'Catalogs'; - String get name => 'Name'; - String get url => 'URL'; - String get duplicate_catalog => 'A catalog with this URL already exists.'; - String get no_catalogs_listed => 'No catalogs listed'; - String get go_back => 'Go Back'; - String get invalid_mokuro_file => 'File is not a Mokuro generated HTML file.'; - String get create_catalog => 'Create Catalog'; - String get adapt_ttu_theme => 'Adapt dictionary popup to theme'; - String get sentence_picker => 'Sentence Picker'; - String field_locked({required Object field}) => '${field} locked and will not clear on export while Creator is active.'; - String field_unlocked({required Object field}) => '${field} unlocked and will clear on export.'; - String get field_lock => 'Lock Field'; - String get field_unlock => 'Unlock Field'; - String get use_dark_theme => 'Use dark theme'; - String get stretch_to_fill_screen => 'Stretch to Fill Screen'; - String get processing_embedded_subtitles => 'Embedded subtitles are processing. Try again later.'; - String get transcript_playback_mode => 'Transcript Playback Mode'; - String get toggle_transcript_background => 'Toggle Transcript Background'; - String get seek => 'Seek'; - String get saved_tags => 'Tags saved.'; - String structured_content_first({required Object i}) => '${i} definitions are unsupported and were omitted.'; - String get structured_content_second => 'Consider a non-structured content version of this dictionary.'; - String get missing_api_key => 'API key not provided'; - String get chatgpt_error => 'There was an error in getting a response from ChatGPT.'; - String get api_key => 'API Key'; - String subtitle_delay_set({required Object ms}) => 'Subtitle delay set to ${ms} ms.'; - String get cancel => 'Cancel'; - String get server_port_in_use => 'Local server port already in use'; - String get google_fonts => 'Google Fonts'; - String get video_show => 'Show video'; - String get video_hide => 'Hide video'; - String get subtitle_timing_show => 'Show subtitle timings'; - String get subtitle_timing_hide => 'Hide subtitle timings'; - String get find_next => 'Find Next'; - String get find_previous => 'Find Previous'; - String get shadowing_mode => 'Shadowing Mode'; - String get display_settings => 'Display Settings'; - String get cloze => 'Cloze'; - String get info_standard_update => 'New standard profile card type'; - String get info_standard_update_content => 'The standard profile now uses the『jidoujisho Kinomoto』 card type.\n\nYour legacy standard profile remains available for backwards compatibility.'; - late final _StringsRetryingInEn retrying_in = _StringsRetryingInEn._(_root); - late final _StringsViewRepliesEn view_replies = _StringsViewRepliesEn._(_root); - String get manage_duplicate_checks => 'Manage Duplicate Checks'; - String get playback_normal => 'Normal Playback Mode'; - String get playback_condensed => 'Condensed Playback Mode'; - String get playback_auto_pause => 'Subtitle Pause Playback Mode'; - String get player_hardware_acceleration => 'Hardware acceleration'; - String get player_use_opensles => 'OpenSL ES audio'; - String get go_forward => 'Go Forward'; - String get browse => 'Browse'; - String get bookmark => 'Bookmark'; - String get add_bookmark => 'Add Bookmark'; - String get add_to_reading_list => 'Add To Reading List'; - String get reading_list_empty => 'Reading list is empty'; - String get reading_list_add_toast => 'Added to reading list.'; - String get reading_list_remove_toast => 'Removed from the reading list.'; - String get ad_block_hosts => 'Ad-block hosts'; - String get error_parsing_hosts_file => 'Error parsing hosts file.'; - String get double_tap_seek_duration => 'Double tap seek duration'; - String get player_background_play => 'Background play'; - String get loaded_from_cache => 'Loaded from web archive cache.'; - String get player_show_subtitle_in_notification => 'Show subtitles in media notification'; - String get subtitles_processing => 'Subtitles are processing...'; - String get video_unavailable => 'Video Unavailable'; - String get video_unavailable_content => 'Cannot fetch streams. There may be restrictions in place that prevent watching this video.'; - String get video_file_error => 'Cannot Load File'; - String get video_file_error_content => 'Unable to load the video file. Please ensure this file exists and is located in a directory accessible by the application.'; + // Translations + String get dictionary_media_type => 'Dictionary'; + String get player_media_type => 'Player'; + String get reader_media_type => 'Reader'; + String get viewer_media_type => 'Viewer'; + String get back => 'Back'; + String get search => 'Search'; + String get search_ellipsis => 'Search...'; + String get show_more => 'Show More'; + String get show_menu => 'Show Menu'; + String get stash => 'Stash'; + String get pick_image => 'Pick Image'; + String get undo => 'Undo'; + String get copy => 'Copy'; + String get clear => 'Clear'; + String get creator => 'Creator'; + String get share => 'Share'; + String get resume_last_media => 'Resume Last Media'; + String get change_source => 'Change Source'; + String get launch_source => 'Launch Source'; + String get card_creator => 'Card Creator'; + String get target_language => 'Target language'; + String get show_options => 'Show Options'; + String get switch_profiles => 'Switch Profiles'; + String get dictionaries => 'Dictionaries'; + String get enhancements => 'Enhancements'; + String get app_locale => 'App locale'; + String get app_locale_warning => + 'Community addons and enhancements are managed by their respective developers, and these may appear in their original language.'; + String get dialog_play => 'PLAY'; + String get dialog_read => 'READ'; + String get dialog_view => 'VIEW'; + String get dialog_edit => 'EDIT'; + String get dialog_export => 'EXPORT'; + String get dialog_import => 'IMPORT'; + String get dialog_close => 'CLOSE'; + String get dialog_clear => 'CLEAR'; + String get dialog_create => 'CREATE'; + String get dialog_delete => 'DELETE'; + String get dialog_cancel => 'CANCEL'; + String get dialog_select => 'SELECT'; + String get dialog_stash => 'STASH'; + String get dialog_search => 'SEARCH'; + String get dialog_exit => 'EXIT'; + String get dialog_share => 'SHARE'; + String get dialog_pop => 'POP'; + String get dialog_save => 'SAVE'; + String get dialog_set => 'SET'; + String get dialog_browse => 'BROWSE'; + String get dialog_channel => 'CHANNEL'; + String get dialog_directory => 'DIRECTORY'; + String get dialog_crop => 'CROP'; + String get dialog_connect => 'CONNECT'; + String get dialog_append => 'APPEND'; + String get dialog_record => 'RECORD'; + String get dialog_manage => 'MANAGE'; + String get dialog_stop => 'STOP'; + String get dialog_done => 'DONE'; + String get reset => 'Reset'; + String get dialog_launch_ankidroid => 'LAUNCH ANKIDROID'; + String get media_item_delete_confirmation => + 'This will clear this item from history. Are you sure you want to do this?'; + String get dictionaries_delete_confirmation => + 'Deleting a dictionary will also clear all dictionary results from history. Are you sure you want to do this?'; + String get mappings_delete_confirmation => + 'This profile will be deleted. Are you sure you want to do this?'; + String get catalog_delete_confirmation => + 'This catalog will be deleted. Are you sure you want to do this?'; + String get dictionaries_deleting_data => 'Deleting dictionary data...'; + String get dictionaries_menu_empty => 'Import a dictionary for use'; + String get options_theme_light => 'Use light theme'; + String get options_theme_dark => 'Use dark theme'; + String get options_incognito_on => 'Turn on incognito mode'; + String get options_incognito_off => 'Turn off incognito mode'; + String get options_dictionaries => 'Manage dictionaries'; + String get options_profiles => 'Export profiles'; + String get options_enhancements => 'User enhancements'; + String get options_language => 'Language settings'; + String get options_github => 'View repository on GitHub'; + String get options_attribution => 'Licenses and attribution'; + String get options_copy => 'Copy'; + String get options_collapse => 'Collapse'; + String get options_expand => 'Expand'; + String get options_delete => 'Delete'; + String get options_show => 'Show'; + String get options_hide => 'Hide'; + String get options_edit => 'Edit'; + String get info_empty_home_tab => 'History is empty'; + String get delete_in_progress => 'Delete in progress'; + String get import_format => 'Import format'; + String get import_in_progress => 'Import in progress'; + String get import_start => 'Preparing for import...'; + String get import_clean => 'Cleaning working space...'; + String get import_extract => 'Extracting files...'; + String import_name({required Object name}) => 'Importing 『${name}』...'; + String get import_entries => 'Processing entries...'; + String import_found_entry({required Object count}) => + 'Found ${count} entries...'; + String import_found_tag({required Object count}) => 'Found ${count} tags...'; + String import_found_frequency({required Object count}) => + 'Found ${count} frequency entries...'; + String import_found_pitch({required Object count}) => + 'Found ${count} pitch accent entries...'; + String import_write_entry({required Object count, required Object total}) => + 'Writing entries:\n${count} / ${total}'; + String import_write_tag({required Object count, required Object total}) => + 'Writing tags:\n${count} / ${total}'; + String import_write_frequency( + {required Object count, required Object total}) => + 'Writing frequency entries:\n${count} / ${total}'; + String import_write_pitch({required Object count, required Object total}) => + 'Writing pitch accent entries:\n${count} / ${total}'; + String get import_failed => 'Dictionary import failed.'; + String get import_complete => 'Dictionary import complete.'; + String import_duplicate({required Object name}) => + 'A dictionary with the name『${name}』is already imported.'; + String get dialog_title_dictionary_clear => 'Clear all dictionaries?'; + String get dialog_content_dictionary_clear => + 'Wiping the dictionary database will also clear all search results in history.'; + String dialog_title_dictionary_delete({required Object name}) => + 'Delete 『${name}』?'; + String get dialog_content_dictionary_delete => + 'Deleting a single dictionary may take longer than clearing the entire dictionary database. This will also clear all search results in history.'; + String get delete_dictionary_data => 'Clearing all dictionary data...'; + String dictionary_tag({required Object name}) => 'Imported from ${name}'; + String get legalese => + 'A full-featured immersion language learning suite for mobile.\n\nOriginally built for the Japanese language learning community by Leo Rafael Orpilla. Logo by suzy and Aaron Marbella.\n\njidoujisho is free and open source software. See the project repository for a comprehensive list of other licenses and attribution notices. Enjoying the application? Help out by providing feedback, making a donation, reporting issues or contributing improvements on GitHub.'; + String get same_name_dictionary_found => 'Dictionary with same name found.'; + String import_file_extension_invalid({required Object extensions}) => + 'This format expects files with the following extensions: ${extensions}'; + String get field_label_empty => 'Empty'; + String get model_to_map => 'Card type to use for new profile'; + String get mapping_name => 'Profile name'; + String get mapping_name_hint => 'Name to assign to profile'; + String get error_profile_name => 'Invalid profile name'; + String get error_profile_name_content => + 'A profile with this name already exists or is not valid and cannot be saved.'; + String get error_standard_profile_name => 'Invalid profile name'; + String get error_standard_profile_name_content => + 'Cannot rename the standard profile.'; + String get error_ankidroid_api => 'AnkiDroid error'; + String get error_ankidroid_api_content => + 'There was an issue communicating with AnkiDroid.\n\nEnsure that the AnkiDroid background service is active and all relevant app permissions are granted in order to continue.'; + String get info_standard_model => 'Standard card type added'; + String get info_standard_model_content => + '『jidoujisho Kinomoto』 has been added to AnkiDroid as a new card type.\n\nSetups making use of a different card type or field order may be used by adding a new export profile.'; + String get error_model_missing => 'Missing card type'; + String get error_model_missing_content => + 'The corresponding card type of the currently selected profile is missing.\n\nThe profile will be deleted, and the standard profile has now been selected in its place.'; + String get error_model_changed => 'Card type changed'; + String get error_model_changed_content => + 'The number of fields of the card type corresponding to the selected profile has changed.\n\nThe fields of the currently selected profile have been reset and will require reconfiguration.'; + String get creator_exporting_as => 'Creating card with profile'; + String get creator_exporting_as_fields_editing => + 'Editing fields for profile'; + String get creator_exporting_as_enhancements_editing => + 'Editing enhancements for profile'; + String get creator_export_card => 'Create Card'; + String get info_enhancements => + 'Enhancements enable the automation of field editing prior to card creation. Pick a slot on the right of a field to allow use of an enhancement. Up to five right slots may be utilised for each field. The enhancement in the left slot of a field will be automatically applied in instant card creation or upon launch of the Card Creator.'; + String get info_actions => + 'Quick actions allow for instant card creation and other automations to be used on dictionary search results. Actions can be assigned via the slots below. Up to six slots may be utilised.'; + String get no_more_available_enhancements => + 'No more available enhancements for this field'; + String get no_more_available_quick_actions => + 'No more available quick actions'; + String get assign_auto_enhancement => 'Assign Auto Enhancement'; + String get assign_manual_enhancement => 'Assign Manual Enhancement'; + String get remove_enhancement => 'Remove Enhancement'; + String copy_of_mapping({required Object name}) => 'Copy of ${name}'; + String get enter_search_term => 'Enter a search term...'; + String searching_for({required Object searchTerm}) => + 'Searching for 『${searchTerm}』...'; + String get no_search_results => 'No search results found.'; + String get edit_actions => 'Edit Dictionary Quick Actions'; + String get remove_action => 'Remove Action'; + String get assign_action => 'Assign Action'; + String dictionary_import_tag({required Object name}) => + 'Imported from ${name}'; + String stash_added_single({required Object term}) => + '『${term}』has been added to the Stash.'; + String get stash_added_multiple => + 'Multiple items have been added to the Stash.'; + String stash_clear_single({required Object term}) => + '『${term}』has been removed from the Stash.'; + String get stash_clear_title => 'Clear Stash'; + String get stash_clear_description => + 'All contents will be cleared. Are you sure?'; + String get stash_placeholder => 'No items in the Stash'; + String get stash_nothing_to_pop => 'No items to be popped from the Stash.'; + String get no_sentences_found => 'No sentences found'; + String get failed_online_service => + 'Failed to communicate with online service'; + String get search_label_before => 'Show all '; + String get search_label_middle => 'out of '; + String get search_label_after => 'search results found for'; + String get clear_dictionary_title => 'Clear Dictionary Result History'; + String get clear_dictionary_description => + 'This will clear all dictionary results from history. Are you sure?'; + String get clear_search_title => 'Clear Search History'; + String get clear_search_description => + 'This will clear all search terms for this history. Are you sure?'; + String get clear_creator_title => 'Clear Creator'; + String get clear_creator_description => + 'This will clear all fields. Are you sure?'; + String get copied_to_clipboard => 'Copied to clipboard.'; + String get no_text => 'No text.'; + String get info_fields => + 'Fields are pre-filled based on the term selected on instant export or prior to opening the Card Creator. In order to include a field for card export, it must be enabled below as well as mapped in the current selected export profile. Enabled fields may also be collapsed below in order to reduce clutter during editing. Use the Clear button on the top-right of the Card Creator in order to wipe these hidden fields quickly when manually editing a card.'; + String get edit_fields => 'Edit and Reorder Fields'; + String get remove_field => 'Remove Field'; + String get add_field => 'Assign Field'; + String get add_field_hint => 'Assign a field to this row'; + String get no_more_available_fields => 'No more available fields'; + String get hidden_fields => 'Additional fields'; + String field_fallback_used( + {required Object field, required Object secondField}) => + 'The ${field} field used ${secondField} as its fallback search term.'; + String get no_text_to_search => 'No text to search.'; + String get image_search_label_before => 'Selecting image '; + String get image_search_label_middle => 'out of '; + String get image_search_label_after => 'found for'; + String get image_search_label_none_middle => 'no image '; + String get image_search_label_none_before => 'Selecting '; + String get preparing_instant_export => 'Preparing card for export...'; + String get processing_in_progress => 'Preparing images'; + String get searching_in_progress => 'Searching for '; + String get audio_unavailable => 'No audio could be found.'; + String get no_audio_enhancements => 'No audio enhancements are assigned.'; + String card_exported({required Object deck}) => 'Card exported to 『${deck}』.'; + String get info_incognito_on => + 'Incognito mode on. Dictionary, media and search history will not be tracked.'; + String get info_incognito_off => + 'Incognito mode off. Dictionary, media and search history will be tracked.'; + String get exit_media_title => 'Exit Media'; + String get exit_media_description => + 'This will return you to the main menu. Are you sure?'; + String get unimplemented_source => 'Unimplemented source'; + String get clear_browser_title => 'Clear Browser Data'; + String get clear_browser_description => + 'This will clear all browsing data used in media sources that use web content. Are you sure?'; + String get ttu_no_books_added => 'No books added to ッツ Ebook Reader'; + String get local_media_directory_empty => 'Directory has no folders or video'; + String get pick_video_file => 'Pick Video File'; + String get pin_player_bottom_bar => 'Pin Player Bottom Bar'; + String get navigate_up_one_directory_level => + 'Navigate Up One Directory Level'; + String get play => 'Play'; + String get pause => 'Pause'; + String get record => 'Record'; + String get stop => 'Stop'; + String get replay => 'Replay'; + String get audio_subtitles => 'Audio/Subtitles'; + String get player_option_shadowing => 'Shadowing Mode'; + String get player_option_change_mode => 'Change Playback Mode'; + String get player_option_listening_comprehension => + 'Listening Comprehension Mode'; + String get player_option_drag_to_select => + 'Use Drag to Select Subtitle Selection'; + String get player_option_tap_to_select => + 'Use Tap to Select Subtitle Selection'; + String get player_option_dictionary_menu => 'Select Active Dictionary Source'; + String get player_option_cast_video => 'Cast to Display Device'; + String get player_option_share_subtitle => 'Share Current Subtitle'; + String get player_option_export => 'Create Card from Context'; + String get player_option_audio => 'Audio'; + String get player_option_subtitle => 'Subtitle'; + String get player_option_subtitle_external => 'External'; + String get player_option_subtitle_none => 'None'; + String get player_option_select_subtitle => 'Select Subtitle Track'; + String get player_option_select_audio => 'Select Audio Track'; + String get player_option_text_filter => 'Use Regular Expression Filter'; + String get player_option_blur_preferences => 'Blur Widget Preferences'; + String get player_option_blur_use => 'Use Blur Widget'; + String get player_option_blur_radius => 'Blur radius'; + String get player_option_blur_options => + 'Set Blur Widget Color and Bluriness'; + String get player_option_blur_reset => 'Reset Blur Widget Size and Position'; + String get player_align_subtitle_transcript => + 'Align Subtitle with Transcript'; + String get player_option_subtitle_appearance => + 'Subtitle Timing and Appearance'; + String get player_option_load_subtitles => 'Load External Subtitles'; + String get player_option_subtitle_delay => 'Subtitle delay'; + String get player_option_audio_allowance => 'Audio allowance'; + String get player_option_font_name => 'Subtitle font name'; + String get player_option_font_color => 'Subtitle font color'; + String get player_option_font_weight => 'Subtitle font weight'; + String get player_option_font_size => 'Subtitle font size'; + String get player_option_outline_color => 'Subtitle outline color'; + String get player_option_regex_filter => 'Regular expression filter'; + String get player_option_subtitle_background_opacity => + 'Subtitle background opacity'; + String get player_option_subtitle_background_blur_radius => + 'Subtitle background blur radius'; + String get player_option_outline_width => 'Subtitle outline width'; + String get player_option_subtitle_always_above_bottom_bar => + 'Always show subtitle above bottom bar area'; + String get player_subtitles_transcript_empty => 'Transcript is empty.'; + String get player_prepare_export => 'Preparing card...'; + String get player_change_player_orientation => 'Change Player Orientation'; + String get no_current_media => 'Play or refresh media for lyrics'; + String get lyrics_permission_required => 'Required permission not granted'; + String get no_lyrics_found => 'No lyrics found'; + String get trending => 'Trending'; + String get caption_filter => 'Filter Closed Captions'; + String get captions_query => 'Querying for captions'; + String get captions_target => 'Target language'; + String get captions_app => 'App language'; + String get captions_other => 'Other language'; + String get captions_closed => 'Closed captioning'; + String get captions_auto => 'Automatic captioning'; + String get captions_unavailable => 'No captioning'; + String get captions_error => 'Error while querying captions'; + String get change_quality => 'Change Quality'; + String get closed_captions_query => 'Querying for captions'; + String get closed_captions_target => 'Target language captions'; + String get closed_captions_app => 'App language captions'; + String get closed_captions_other => 'Other language captions'; + String get closed_captions_unavailable => 'No captions'; + String get closed_captions_error => 'Error while querying captions'; + String get stream_url => 'Stream URL'; + String get default_option => 'Default'; + String get paste => 'Paste'; + String get lyrics_title => 'Title'; + String get lyrics_artist => 'Artist'; + String get set_media => 'Set Media'; + String get no_recordings_found => 'No recordings found'; + String get wrap_image_audio => 'Include image/audio HTML tags on export'; + String get server_address => 'Server Address'; + String get no_active_connection => 'No active connection'; + String get failed_server_connection => 'Failed to connect to server'; + String get no_text_received => 'No text received'; + String get text_segmentation => 'Text Segmentation'; + String get connect_disconnect => 'Connect/Disconnect'; + String get clear_text_title => 'Clear Text'; + String get clear_text_description => + 'This will clear all received text. Are you sure?'; + String get close_connection_title => 'Close Connection'; + String get close_connection_description => + 'This will end the WebSocket connection and clear all received text. Are you sure?'; + String get use_slow_import => 'Slow import (use if failing)'; + String get settings => 'Settings'; + String get manager => 'Manager'; + String get volume_button_page_turning => 'Volume button page turning'; + String get invert_volume_buttons => 'Invert volume buttons'; + String get volume_button_turning_speed => 'Continuous scrolling speed'; + String get extend_page_beyond_navbar => 'Extend page beyond navigation bar'; + String get tweaks => 'Tweaks'; + String get increase => 'Increase'; + String get decrease => 'Decrease'; + String get unit_milliseconds => 'ms'; + String get unit_pixels => 'px'; + String get dictionary_settings => 'Dictionary Settings'; + String get auto_search => 'Auto search'; + String get auto_search_debounce_delay => 'Auto search debounce delay'; + String get dictionary_font_size => 'Dictionary font size'; + String get close_on_export => 'Close on Export'; + String get close_on_export_on => + 'The Card Creator will now automatically close upon card export.'; + String get close_on_export_off => + 'The Card Creator will no longer close upon card export.'; + String get export_profile_empty => + 'Your export profile has no set fields and requires configuration.'; + String get error_export_media_ankidroid => + 'There was an error in exporting media to AnkiDroid.'; + String get error_add_note => + 'There was an error in adding a note to AnkiDroid.'; + String get first_time_setup => 'First-Time Setup'; + String get first_time_setup_description => + 'Welcome to jidoujisho! Set your target language and a default profile will be tailored for you. You can change this later at anytime.'; + String get maximum_entries => 'Maximum dictionary entry query limit'; + String get maximum_terms => 'Maximum dictionary headwords in result'; + String get use_br_tags => 'Use line break tag instead of newline on export'; + String get prepend_dictionary_names => 'Prepend dictionary name in meaning'; + String get highlight_on_tap => 'Highlight text on tap'; + String get no_audio_file => 'No audio file to save.'; + String get storage_permissions => + 'Please grant the following permissions for exporting to AnkiDroid.'; + String get stream => 'Stream'; + String get network_subtitles_warning => + 'Embedded subtitles are unsupported for network streams.'; + String get accessibility => + 'Permission is required to capture text from accessibility events.'; + String get comments => 'Comments'; + String get replies => 'Replies'; + String get no_comments_queried => 'No comments queried'; + String get no_text_in_clipboard => 'No text to display'; + String file_downloaded({required Object name}) => 'File downloaded: ${name}'; + String get cfhange_sort_order => 'Change Sort Order'; + String get login => 'Login'; + String get send => 'Send'; + String get no_messages => 'Start a chat'; + String get enter_message => 'Enter message...'; + String get clear_message_title => 'Clear Messages'; + String get clear_message_description => + 'This will clear all messages and start a new chat. Are you sure?'; + String get error_chatgpt_response => + 'Request failed or rate-limited. Try again shortly or check your usage limits.'; + String get pick_file => 'Pick File'; + String get open_url => 'Open URL'; + String get catalogs => 'Catalogs'; + String get name => 'Name'; + String get url => 'URL'; + String get duplicate_catalog => 'A catalog with this URL already exists.'; + String get no_catalogs_listed => 'No catalogs listed'; + String get go_back => 'Go Back'; + String get invalid_mokuro_file => 'File is not a Mokuro generated HTML file.'; + String get create_catalog => 'Create Catalog'; + String get adapt_ttu_theme => 'Adapt dictionary popup to theme'; + String get sentence_picker => 'Sentence Picker'; + String field_locked({required Object field}) => + '${field} locked and will not clear on export while Creator is active.'; + String field_unlocked({required Object field}) => + '${field} unlocked and will clear on export.'; + String get field_lock => 'Lock Field'; + String get field_unlock => 'Unlock Field'; + String get use_dark_theme => 'Use dark theme'; + String get stretch_to_fill_screen => 'Stretch to Fill Screen'; + String get processing_embedded_subtitles => + 'Embedded subtitles are processing. Try again later.'; + String get transcript_playback_mode => 'Transcript Playback Mode'; + String get toggle_transcript_background => 'Toggle Transcript Background'; + String get seek => 'Seek'; + String get saved_tags => 'Tags saved.'; + String structured_content_first({required Object i}) => + '${i} definitions are unsupported and were omitted.'; + String get structured_content_second => + 'Consider a non-structured content version of this dictionary.'; + String get missing_api_key => 'API key not provided'; + String get chatgpt_error => + 'There was an error in getting a response from ChatGPT.'; + String get api_key => 'API Key'; + String subtitle_delay_set({required Object ms}) => + 'Subtitle delay set to ${ms} ms.'; + String get cancel => 'Cancel'; + String get server_port_in_use => 'Local server port already in use'; + String get google_fonts => 'Google Fonts'; + String get video_show => 'Show video'; + String get video_hide => 'Hide video'; + String get subtitle_timing_show => 'Show subtitle timings'; + String get subtitle_timing_hide => 'Hide subtitle timings'; + String get find_next => 'Find Next'; + String get find_previous => 'Find Previous'; + String get shadowing_mode => 'Shadowing Mode'; + String get display_settings => 'Display Settings'; + String get cloze => 'Cloze'; + String get info_standard_update => 'New standard profile card type'; + String get info_standard_update_content => + 'The standard profile now uses the『jidoujisho Kinomoto』 card type.\n\nYour legacy standard profile remains available for backwards compatibility.'; + late final _StringsRetryingInEn retrying_in = _StringsRetryingInEn._(_root); + late final _StringsViewRepliesEn view_replies = + _StringsViewRepliesEn._(_root); + String get manage_duplicate_checks => 'Manage Duplicate Checks'; + String get playback_normal => 'Normal Playback Mode'; + String get playback_condensed => 'Condensed Playback Mode'; + String get playback_auto_pause => 'Subtitle Pause Playback Mode'; + String get player_hardware_acceleration => 'Hardware acceleration'; + String get player_use_opensles => 'OpenSL ES audio'; + String get go_forward => 'Go Forward'; + String get browse => 'Browse'; + String get bookmark => 'Bookmark'; + String get add_bookmark => 'Add Bookmark'; + String get add_to_reading_list => 'Add To Reading List'; + String get reading_list_empty => 'Reading list is empty'; + String get reading_list_add_toast => 'Added to reading list.'; + String get reading_list_remove_toast => 'Removed from the reading list.'; + String get ad_block_hosts => 'Ad-block hosts'; + String get error_parsing_hosts_file => 'Error parsing hosts file.'; + String get double_tap_seek_duration => 'Double tap seek duration'; + String get player_background_play => 'Background play'; + String get loaded_from_cache => 'Loaded from web archive cache.'; + String get player_show_subtitle_in_notification => + 'Show subtitles in media notification'; + String get subtitles_processing => 'Subtitles are processing...'; + String get video_unavailable => 'Video Unavailable'; + String get video_unavailable_content => + 'Cannot fetch streams. There may be restrictions in place that prevent watching this video.'; + String get video_file_error => 'Cannot Load File'; + String get video_file_error_content => + 'Unable to load the video file. Please ensure this file exists and is located in a directory accessible by the application.'; } // Path: retrying_in class _StringsRetryingInEn { - _StringsRetryingInEn._(this._root); + _StringsRetryingInEn._(this._root); - final _StringsEn _root; // ignore: unused_field + final _StringsEn _root; // ignore: unused_field - // Translations - String seconds({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - one: 'Retrying in ${n} second...', - other: 'Retrying in ${n} seconds...', - ); + // Translations + String seconds({required num n}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))( + n, + one: 'Retrying in ${n} second...', + other: 'Retrying in ${n} seconds...', + ); } // Path: view_replies class _StringsViewRepliesEn { - _StringsViewRepliesEn._(this._root); + _StringsViewRepliesEn._(this._root); - final _StringsEn _root; // ignore: unused_field + final _StringsEn _root; // ignore: unused_field - // Translations - String reply({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - one: 'SHOW ${n} REPLY', - other: 'SHOW ${n} REPLIES', - ); + // Translations + String reply({required num n}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))( + n, + one: 'SHOW ${n} REPLY', + other: 'SHOW ${n} REPLIES', + ); } /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. extension on _StringsEn { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'dictionary_media_type': return 'Dictionary'; - case 'player_media_type': return 'Player'; - case 'reader_media_type': return 'Reader'; - case 'viewer_media_type': return 'Viewer'; - case 'back': return 'Back'; - case 'search': return 'Search'; - case 'search_ellipsis': return 'Search...'; - case 'show_more': return 'Show More'; - case 'show_menu': return 'Show Menu'; - case 'stash': return 'Stash'; - case 'pick_image': return 'Pick Image'; - case 'undo': return 'Undo'; - case 'copy': return 'Copy'; - case 'clear': return 'Clear'; - case 'creator': return 'Creator'; - case 'share': return 'Share'; - case 'resume_last_media': return 'Resume Last Media'; - case 'change_source': return 'Change Source'; - case 'launch_source': return 'Launch Source'; - case 'card_creator': return 'Card Creator'; - case 'target_language': return 'Target language'; - case 'show_options': return 'Show Options'; - case 'switch_profiles': return 'Switch Profiles'; - case 'dictionaries': return 'Dictionaries'; - case 'enhancements': return 'Enhancements'; - case 'app_locale': return 'App locale'; - case 'app_locale_warning': return 'Community addons and enhancements are managed by their respective developers, and these may appear in their original language.'; - case 'dialog_play': return 'PLAY'; - case 'dialog_read': return 'READ'; - case 'dialog_view': return 'VIEW'; - case 'dialog_edit': return 'EDIT'; - case 'dialog_export': return 'EXPORT'; - case 'dialog_import': return 'IMPORT'; - case 'dialog_close': return 'CLOSE'; - case 'dialog_clear': return 'CLEAR'; - case 'dialog_create': return 'CREATE'; - case 'dialog_delete': return 'DELETE'; - case 'dialog_cancel': return 'CANCEL'; - case 'dialog_select': return 'SELECT'; - case 'dialog_stash': return 'STASH'; - case 'dialog_search': return 'SEARCH'; - case 'dialog_exit': return 'EXIT'; - case 'dialog_share': return 'SHARE'; - case 'dialog_pop': return 'POP'; - case 'dialog_save': return 'SAVE'; - case 'dialog_set': return 'SET'; - case 'dialog_browse': return 'BROWSE'; - case 'dialog_channel': return 'CHANNEL'; - case 'dialog_directory': return 'DIRECTORY'; - case 'dialog_crop': return 'CROP'; - case 'dialog_connect': return 'CONNECT'; - case 'dialog_append': return 'APPEND'; - case 'dialog_record': return 'RECORD'; - case 'dialog_manage': return 'MANAGE'; - case 'dialog_stop': return 'STOP'; - case 'dialog_done': return 'DONE'; - case 'reset': return 'Reset'; - case 'dialog_launch_ankidroid': return 'LAUNCH ANKIDROID'; - case 'media_item_delete_confirmation': return 'This will clear this item from history. Are you sure you want to do this?'; - case 'dictionaries_delete_confirmation': return 'Deleting a dictionary will also clear all dictionary results from history. Are you sure you want to do this?'; - case 'mappings_delete_confirmation': return 'This profile will be deleted. Are you sure you want to do this?'; - case 'catalog_delete_confirmation': return 'This catalog will be deleted. Are you sure you want to do this?'; - case 'dictionaries_deleting_data': return 'Deleting dictionary data...'; - case 'dictionaries_menu_empty': return 'Import a dictionary for use'; - case 'options_theme_light': return 'Use light theme'; - case 'options_theme_dark': return 'Use dark theme'; - case 'options_incognito_on': return 'Turn on incognito mode'; - case 'options_incognito_off': return 'Turn off incognito mode'; - case 'options_dictionaries': return 'Manage dictionaries'; - case 'options_profiles': return 'Export profiles'; - case 'options_enhancements': return 'User enhancements'; - case 'options_language': return 'Language settings'; - case 'options_github': return 'View repository on GitHub'; - case 'options_attribution': return 'Licenses and attribution'; - case 'options_copy': return 'Copy'; - case 'options_collapse': return 'Collapse'; - case 'options_expand': return 'Expand'; - case 'options_delete': return 'Delete'; - case 'options_show': return 'Show'; - case 'options_hide': return 'Hide'; - case 'options_edit': return 'Edit'; - case 'info_empty_home_tab': return 'History is empty'; - case 'delete_in_progress': return 'Delete in progress'; - case 'import_format': return 'Import format'; - case 'import_in_progress': return 'Import in progress'; - case 'import_start': return 'Preparing for import...'; - case 'import_clean': return 'Cleaning working space...'; - case 'import_extract': return 'Extracting files...'; - case 'import_name': return ({required Object name}) => 'Importing 『${name}』...'; - case 'import_entries': return 'Processing entries...'; - case 'import_found_entry': return ({required Object count}) => 'Found ${count} entries...'; - case 'import_found_tag': return ({required Object count}) => 'Found ${count} tags...'; - case 'import_found_frequency': return ({required Object count}) => 'Found ${count} frequency entries...'; - case 'import_found_pitch': return ({required Object count}) => 'Found ${count} pitch accent entries...'; - case 'import_write_entry': return ({required Object count, required Object total}) => 'Writing entries:\n${count} / ${total}'; - case 'import_write_tag': return ({required Object count, required Object total}) => 'Writing tags:\n${count} / ${total}'; - case 'import_write_frequency': return ({required Object count, required Object total}) => 'Writing frequency entries:\n${count} / ${total}'; - case 'import_write_pitch': return ({required Object count, required Object total}) => 'Writing pitch accent entries:\n${count} / ${total}'; - case 'import_failed': return 'Dictionary import failed.'; - case 'import_complete': return 'Dictionary import complete.'; - case 'import_duplicate': return ({required Object name}) => 'A dictionary with the name『${name}』is already imported.'; - case 'dialog_title_dictionary_clear': return 'Clear all dictionaries?'; - case 'dialog_content_dictionary_clear': return 'Wiping the dictionary database will also clear all search results in history.'; - case 'dialog_title_dictionary_delete': return ({required Object name}) => 'Delete 『${name}』?'; - case 'dialog_content_dictionary_delete': return 'Deleting a single dictionary may take longer than clearing the entire dictionary database. This will also clear all search results in history.'; - case 'delete_dictionary_data': return 'Clearing all dictionary data...'; - case 'dictionary_tag': return ({required Object name}) => 'Imported from ${name}'; - case 'legalese': return 'A full-featured immersion language learning suite for mobile.\n\nOriginally built for the Japanese language learning community by Leo Rafael Orpilla. Logo by suzy and Aaron Marbella.\n\njidoujisho is free and open source software. See the project repository for a comprehensive list of other licenses and attribution notices. Enjoying the application? Help out by providing feedback, making a donation, reporting issues or contributing improvements on GitHub.'; - case 'same_name_dictionary_found': return 'Dictionary with same name found.'; - case 'import_file_extension_invalid': return ({required Object extensions}) => 'This format expects files with the following extensions: ${extensions}'; - case 'field_label_empty': return 'Empty'; - case 'model_to_map': return 'Card type to use for new profile'; - case 'mapping_name': return 'Profile name'; - case 'mapping_name_hint': return 'Name to assign to profile'; - case 'error_profile_name': return 'Invalid profile name'; - case 'error_profile_name_content': return 'A profile with this name already exists or is not valid and cannot be saved.'; - case 'error_standard_profile_name': return 'Invalid profile name'; - case 'error_standard_profile_name_content': return 'Cannot rename the standard profile.'; - case 'error_ankidroid_api': return 'AnkiDroid error'; - case 'error_ankidroid_api_content': return 'There was an issue communicating with AnkiDroid.\n\nEnsure that the AnkiDroid background service is active and all relevant app permissions are granted in order to continue.'; - case 'info_standard_model': return 'Standard card type added'; - case 'info_standard_model_content': return '『jidoujisho Kinomoto』 has been added to AnkiDroid as a new card type.\n\nSetups making use of a different card type or field order may be used by adding a new export profile.'; - case 'error_model_missing': return 'Missing card type'; - case 'error_model_missing_content': return 'The corresponding card type of the currently selected profile is missing.\n\nThe profile will be deleted, and the standard profile has now been selected in its place.'; - case 'error_model_changed': return 'Card type changed'; - case 'error_model_changed_content': return 'The number of fields of the card type corresponding to the selected profile has changed.\n\nThe fields of the currently selected profile have been reset and will require reconfiguration.'; - case 'creator_exporting_as': return 'Creating card with profile'; - case 'creator_exporting_as_fields_editing': return 'Editing fields for profile'; - case 'creator_exporting_as_enhancements_editing': return 'Editing enhancements for profile'; - case 'creator_export_card': return 'Create Card'; - case 'info_enhancements': return 'Enhancements enable the automation of field editing prior to card creation. Pick a slot on the right of a field to allow use of an enhancement. Up to five right slots may be utilised for each field. The enhancement in the left slot of a field will be automatically applied in instant card creation or upon launch of the Card Creator.'; - case 'info_actions': return 'Quick actions allow for instant card creation and other automations to be used on dictionary search results. Actions can be assigned via the slots below. Up to six slots may be utilised.'; - case 'no_more_available_enhancements': return 'No more available enhancements for this field'; - case 'no_more_available_quick_actions': return 'No more available quick actions'; - case 'assign_auto_enhancement': return 'Assign Auto Enhancement'; - case 'assign_manual_enhancement': return 'Assign Manual Enhancement'; - case 'remove_enhancement': return 'Remove Enhancement'; - case 'copy_of_mapping': return ({required Object name}) => 'Copy of ${name}'; - case 'enter_search_term': return 'Enter a search term...'; - case 'searching_for': return ({required Object searchTerm}) => 'Searching for 『${searchTerm}』...'; - case 'no_search_results': return 'No search results found.'; - case 'edit_actions': return 'Edit Dictionary Quick Actions'; - case 'remove_action': return 'Remove Action'; - case 'assign_action': return 'Assign Action'; - case 'dictionary_import_tag': return ({required Object name}) => 'Imported from ${name}'; - case 'stash_added_single': return ({required Object term}) => '『${term}』has been added to the Stash.'; - case 'stash_added_multiple': return 'Multiple items have been added to the Stash.'; - case 'stash_clear_single': return ({required Object term}) => '『${term}』has been removed from the Stash.'; - case 'stash_clear_title': return 'Clear Stash'; - case 'stash_clear_description': return 'All contents will be cleared. Are you sure?'; - case 'stash_placeholder': return 'No items in the Stash'; - case 'stash_nothing_to_pop': return 'No items to be popped from the Stash.'; - case 'no_sentences_found': return 'No sentences found'; - case 'failed_online_service': return 'Failed to communicate with online service'; - case 'search_label_before': return 'Show all '; - case 'search_label_middle': return 'out of '; - case 'search_label_after': return 'search results found for'; - case 'clear_dictionary_title': return 'Clear Dictionary Result History'; - case 'clear_dictionary_description': return 'This will clear all dictionary results from history. Are you sure?'; - case 'clear_search_title': return 'Clear Search History'; - case 'clear_search_description': return 'This will clear all search terms for this history. Are you sure?'; - case 'clear_creator_title': return 'Clear Creator'; - case 'clear_creator_description': return 'This will clear all fields. Are you sure?'; - case 'copied_to_clipboard': return 'Copied to clipboard.'; - case 'no_text': return 'No text.'; - case 'info_fields': return 'Fields are pre-filled based on the term selected on instant export or prior to opening the Card Creator. In order to include a field for card export, it must be enabled below as well as mapped in the current selected export profile. Enabled fields may also be collapsed below in order to reduce clutter during editing. Use the Clear button on the top-right of the Card Creator in order to wipe these hidden fields quickly when manually editing a card.'; - case 'edit_fields': return 'Edit and Reorder Fields'; - case 'remove_field': return 'Remove Field'; - case 'add_field': return 'Assign Field'; - case 'add_field_hint': return 'Assign a field to this row'; - case 'no_more_available_fields': return 'No more available fields'; - case 'hidden_fields': return 'Additional fields'; - case 'field_fallback_used': return ({required Object field, required Object secondField}) => 'The ${field} field used ${secondField} as its fallback search term.'; - case 'no_text_to_search': return 'No text to search.'; - case 'image_search_label_before': return 'Selecting image '; - case 'image_search_label_middle': return 'out of '; - case 'image_search_label_after': return 'found for'; - case 'image_search_label_none_middle': return 'no image '; - case 'image_search_label_none_before': return 'Selecting '; - case 'preparing_instant_export': return 'Preparing card for export...'; - case 'processing_in_progress': return 'Preparing images'; - case 'searching_in_progress': return 'Searching for '; - case 'audio_unavailable': return 'No audio could be found.'; - case 'no_audio_enhancements': return 'No audio enhancements are assigned.'; - case 'card_exported': return ({required Object deck}) => 'Card exported to 『${deck}』.'; - case 'info_incognito_on': return 'Incognito mode on. Dictionary, media and search history will not be tracked.'; - case 'info_incognito_off': return 'Incognito mode off. Dictionary, media and search history will be tracked.'; - case 'exit_media_title': return 'Exit Media'; - case 'exit_media_description': return 'This will return you to the main menu. Are you sure?'; - case 'unimplemented_source': return 'Unimplemented source'; - case 'clear_browser_title': return 'Clear Browser Data'; - case 'clear_browser_description': return 'This will clear all browsing data used in media sources that use web content. Are you sure?'; - case 'ttu_no_books_added': return 'No books added to ッツ Ebook Reader'; - case 'local_media_directory_empty': return 'Directory has no folders or video'; - case 'pick_video_file': return 'Pick Video File'; - case 'navigate_up_one_directory_level': return 'Navigate Up One Directory Level'; - case 'play': return 'Play'; - case 'pause': return 'Pause'; - case 'record': return 'Record'; - case 'stop': return 'Stop'; - case 'replay': return 'Replay'; - case 'audio_subtitles': return 'Audio/Subtitles'; - case 'player_option_shadowing': return 'Shadowing Mode'; - case 'player_option_change_mode': return 'Change Playback Mode'; - case 'player_option_listening_comprehension': return 'Listening Comprehension Mode'; - case 'player_option_drag_to_select': return 'Use Drag to Select Subtitle Selection'; - case 'player_option_tap_to_select': return 'Use Tap to Select Subtitle Selection'; - case 'player_option_dictionary_menu': return 'Select Active Dictionary Source'; - case 'player_option_cast_video': return 'Cast to Display Device'; - case 'player_option_share_subtitle': return 'Share Current Subtitle'; - case 'player_option_export': return 'Create Card from Context'; - case 'player_option_audio': return 'Audio'; - case 'player_option_subtitle': return 'Subtitle'; - case 'player_option_subtitle_external': return 'External'; - case 'player_option_subtitle_none': return 'None'; - case 'player_option_select_subtitle': return 'Select Subtitle Track'; - case 'player_option_select_audio': return 'Select Audio Track'; - case 'player_option_text_filter': return 'Use Regular Expression Filter'; - case 'player_option_blur_preferences': return 'Blur Widget Preferences'; - case 'player_option_blur_use': return 'Use Blur Widget'; - case 'player_option_blur_radius': return 'Blur radius'; - case 'player_option_blur_options': return 'Set Blur Widget Color and Bluriness'; - case 'player_option_blur_reset': return 'Reset Blur Widget Size and Position'; - case 'player_align_subtitle_transcript': return 'Align Subtitle with Transcript'; - case 'player_option_subtitle_appearance': return 'Subtitle Timing and Appearance'; - case 'player_option_load_subtitles': return 'Load External Subtitles'; - case 'player_option_subtitle_delay': return 'Subtitle delay'; - case 'player_option_audio_allowance': return 'Audio allowance'; - case 'player_option_font_name': return 'Subtitle font name'; - case 'player_option_font_size': return 'Subtitle font size'; - case 'player_option_regex_filter': return 'Regular expression filter'; - case 'player_option_subtitle_background_opacity': return 'Subtitle background opacity'; - case 'player_option_subtitle_background_blur_radius': return 'Subtitle background blur radius'; - case 'player_option_outline_width': return 'Subtitle outline width'; - case 'player_option_subtitle_always_above_bottom_bar': return 'Always show subtitle above bottom bar area'; - case 'player_subtitles_transcript_empty': return 'Transcript is empty.'; - case 'player_prepare_export': return 'Preparing card...'; - case 'player_change_player_orientation': return 'Change Player Orientation'; - case 'no_current_media': return 'Play or refresh media for lyrics'; - case 'lyrics_permission_required': return 'Required permission not granted'; - case 'no_lyrics_found': return 'No lyrics found'; - case 'trending': return 'Trending'; - case 'caption_filter': return 'Filter Closed Captions'; - case 'captions_query': return 'Querying for captions'; - case 'captions_target': return 'Target language'; - case 'captions_app': return 'App language'; - case 'captions_other': return 'Other language'; - case 'captions_closed': return 'Closed captioning'; - case 'captions_auto': return 'Automatic captioning'; - case 'captions_unavailable': return 'No captioning'; - case 'captions_error': return 'Error while querying captions'; - case 'change_quality': return 'Change Quality'; - case 'closed_captions_query': return 'Querying for captions'; - case 'closed_captions_target': return 'Target language captions'; - case 'closed_captions_app': return 'App language captions'; - case 'closed_captions_other': return 'Other language captions'; - case 'closed_captions_unavailable': return 'No captions'; - case 'closed_captions_error': return 'Error while querying captions'; - case 'stream_url': return 'Stream URL'; - case 'default_option': return 'Default'; - case 'paste': return 'Paste'; - case 'lyrics_title': return 'Title'; - case 'lyrics_artist': return 'Artist'; - case 'set_media': return 'Set Media'; - case 'no_recordings_found': return 'No recordings found'; - case 'wrap_image_audio': return 'Include image/audio HTML tags on export'; - case 'server_address': return 'Server Address'; - case 'no_active_connection': return 'No active connection'; - case 'failed_server_connection': return 'Failed to connect to server'; - case 'no_text_received': return 'No text received'; - case 'text_segmentation': return 'Text Segmentation'; - case 'connect_disconnect': return 'Connect/Disconnect'; - case 'clear_text_title': return 'Clear Text'; - case 'clear_text_description': return 'This will clear all received text. Are you sure?'; - case 'close_connection_title': return 'Close Connection'; - case 'close_connection_description': return 'This will end the WebSocket connection and clear all received text. Are you sure?'; - case 'use_slow_import': return 'Slow import (use if failing)'; - case 'settings': return 'Settings'; - case 'manager': return 'Manager'; - case 'volume_button_page_turning': return 'Volume button page turning'; - case 'invert_volume_buttons': return 'Invert volume buttons'; - case 'volume_button_turning_speed': return 'Continuous scrolling speed'; - case 'extend_page_beyond_navbar': return 'Extend page beyond navigation bar'; - case 'tweaks': return 'Tweaks'; - case 'increase': return 'Increase'; - case 'decrease': return 'Decrease'; - case 'unit_milliseconds': return 'ms'; - case 'unit_pixels': return 'px'; - case 'dictionary_settings': return 'Dictionary Settings'; - case 'auto_search': return 'Auto search'; - case 'auto_search_debounce_delay': return 'Auto search debounce delay'; - case 'dictionary_font_size': return 'Dictionary font size'; - case 'close_on_export': return 'Close on Export'; - case 'close_on_export_on': return 'The Card Creator will now automatically close upon card export.'; - case 'close_on_export_off': return 'The Card Creator will no longer close upon card export.'; - case 'export_profile_empty': return 'Your export profile has no set fields and requires configuration.'; - case 'error_export_media_ankidroid': return 'There was an error in exporting media to AnkiDroid.'; - case 'error_add_note': return 'There was an error in adding a note to AnkiDroid.'; - case 'first_time_setup': return 'First-Time Setup'; - case 'first_time_setup_description': return 'Welcome to jidoujisho! Set your target language and a default profile will be tailored for you. You can change this later at anytime.'; - case 'maximum_entries': return 'Maximum dictionary entry query limit'; - case 'maximum_terms': return 'Maximum dictionary headwords in result'; - case 'use_br_tags': return 'Use line break tag instead of newline on export'; - case 'prepend_dictionary_names': return 'Prepend dictionary name in meaning'; - case 'highlight_on_tap': return 'Highlight text on tap'; - case 'no_audio_file': return 'No audio file to save.'; - case 'storage_permissions': return 'Please grant the following permissions for exporting to AnkiDroid.'; - case 'stream': return 'Stream'; - case 'network_subtitles_warning': return 'Embedded subtitles are unsupported for network streams.'; - case 'accessibility': return 'Permission is required to capture text from accessibility events.'; - case 'comments': return 'Comments'; - case 'replies': return 'Replies'; - case 'no_comments_queried': return 'No comments queried'; - case 'no_text_in_clipboard': return 'No text to display'; - case 'file_downloaded': return ({required Object name}) => 'File downloaded: ${name}'; - case 'cfhange_sort_order': return 'Change Sort Order'; - case 'login': return 'Login'; - case 'send': return 'Send'; - case 'no_messages': return 'Start a chat'; - case 'enter_message': return 'Enter message...'; - case 'clear_message_title': return 'Clear Messages'; - case 'clear_message_description': return 'This will clear all messages and start a new chat. Are you sure?'; - case 'error_chatgpt_response': return 'Request failed or rate-limited. Try again shortly or check your usage limits.'; - case 'pick_file': return 'Pick File'; - case 'open_url': return 'Open URL'; - case 'catalogs': return 'Catalogs'; - case 'name': return 'Name'; - case 'url': return 'URL'; - case 'duplicate_catalog': return 'A catalog with this URL already exists.'; - case 'no_catalogs_listed': return 'No catalogs listed'; - case 'go_back': return 'Go Back'; - case 'invalid_mokuro_file': return 'File is not a Mokuro generated HTML file.'; - case 'create_catalog': return 'Create Catalog'; - case 'adapt_ttu_theme': return 'Adapt dictionary popup to theme'; - case 'sentence_picker': return 'Sentence Picker'; - case 'field_locked': return ({required Object field}) => '${field} locked and will not clear on export while Creator is active.'; - case 'field_unlocked': return ({required Object field}) => '${field} unlocked and will clear on export.'; - case 'field_lock': return 'Lock Field'; - case 'field_unlock': return 'Unlock Field'; - case 'use_dark_theme': return 'Use dark theme'; - case 'stretch_to_fill_screen': return 'Stretch to Fill Screen'; - case 'processing_embedded_subtitles': return 'Embedded subtitles are processing. Try again later.'; - case 'transcript_playback_mode': return 'Transcript Playback Mode'; - case 'toggle_transcript_background': return 'Toggle Transcript Background'; - case 'seek': return 'Seek'; - case 'saved_tags': return 'Tags saved.'; - case 'structured_content_first': return ({required Object i}) => '${i} definitions are unsupported and were omitted.'; - case 'structured_content_second': return 'Consider a non-structured content version of this dictionary.'; - case 'missing_api_key': return 'API key not provided'; - case 'chatgpt_error': return 'There was an error in getting a response from ChatGPT.'; - case 'api_key': return 'API Key'; - case 'subtitle_delay_set': return ({required Object ms}) => 'Subtitle delay set to ${ms} ms.'; - case 'cancel': return 'Cancel'; - case 'server_port_in_use': return 'Local server port already in use'; - case 'google_fonts': return 'Google Fonts'; - case 'video_show': return 'Show video'; - case 'video_hide': return 'Hide video'; - case 'subtitle_timing_show': return 'Show subtitle timings'; - case 'subtitle_timing_hide': return 'Hide subtitle timings'; - case 'find_next': return 'Find Next'; - case 'find_previous': return 'Find Previous'; - case 'shadowing_mode': return 'Shadowing Mode'; - case 'display_settings': return 'Display Settings'; - case 'cloze': return 'Cloze'; - case 'info_standard_update': return 'New standard profile card type'; - case 'info_standard_update_content': return 'The standard profile now uses the『jidoujisho Kinomoto』 card type.\n\nYour legacy standard profile remains available for backwards compatibility.'; - case 'retrying_in.seconds': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - one: 'Retrying in ${n} second...', - other: 'Retrying in ${n} seconds...', - ); - case 'view_replies.reply': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - one: 'SHOW ${n} REPLY', - other: 'SHOW ${n} REPLIES', - ); - case 'manage_duplicate_checks': return 'Manage Duplicate Checks'; - case 'playback_normal': return 'Normal Playback Mode'; - case 'playback_condensed': return 'Condensed Playback Mode'; - case 'playback_auto_pause': return 'Subtitle Pause Playback Mode'; - case 'player_hardware_acceleration': return 'Hardware acceleration'; - case 'player_use_opensles': return 'OpenSL ES audio'; - case 'go_forward': return 'Go Forward'; - case 'browse': return 'Browse'; - case 'bookmark': return 'Bookmark'; - case 'add_bookmark': return 'Add Bookmark'; - case 'add_to_reading_list': return 'Add To Reading List'; - case 'reading_list_empty': return 'Reading list is empty'; - case 'reading_list_add_toast': return 'Added to reading list.'; - case 'reading_list_remove_toast': return 'Removed from the reading list.'; - case 'ad_block_hosts': return 'Ad-block hosts'; - case 'error_parsing_hosts_file': return 'Error parsing hosts file.'; - case 'double_tap_seek_duration': return 'Double tap seek duration'; - case 'player_background_play': return 'Background play'; - case 'loaded_from_cache': return 'Loaded from web archive cache.'; - case 'player_show_subtitle_in_notification': return 'Show subtitles in media notification'; - case 'subtitles_processing': return 'Subtitles are processing...'; - case 'video_unavailable': return 'Video Unavailable'; - case 'video_unavailable_content': return 'Cannot fetch streams. There may be restrictions in place that prevent watching this video.'; - case 'video_file_error': return 'Cannot Load File'; - case 'video_file_error_content': return 'Unable to load the video file. Please ensure this file exists and is located in a directory accessible by the application.'; - default: return null; - } - } + dynamic _flatMapFunction(String path) { + switch (path) { + case 'dictionary_media_type': + return 'Dictionary'; + case 'player_media_type': + return 'Player'; + case 'reader_media_type': + return 'Reader'; + case 'viewer_media_type': + return 'Viewer'; + case 'back': + return 'Back'; + case 'search': + return 'Search'; + case 'search_ellipsis': + return 'Search...'; + case 'show_more': + return 'Show More'; + case 'show_menu': + return 'Show Menu'; + case 'stash': + return 'Stash'; + case 'pick_image': + return 'Pick Image'; + case 'undo': + return 'Undo'; + case 'copy': + return 'Copy'; + case 'clear': + return 'Clear'; + case 'creator': + return 'Creator'; + case 'share': + return 'Share'; + case 'resume_last_media': + return 'Resume Last Media'; + case 'change_source': + return 'Change Source'; + case 'launch_source': + return 'Launch Source'; + case 'card_creator': + return 'Card Creator'; + case 'target_language': + return 'Target language'; + case 'show_options': + return 'Show Options'; + case 'switch_profiles': + return 'Switch Profiles'; + case 'dictionaries': + return 'Dictionaries'; + case 'enhancements': + return 'Enhancements'; + case 'app_locale': + return 'App locale'; + case 'app_locale_warning': + return 'Community addons and enhancements are managed by their respective developers, and these may appear in their original language.'; + case 'dialog_play': + return 'PLAY'; + case 'dialog_read': + return 'READ'; + case 'dialog_view': + return 'VIEW'; + case 'dialog_edit': + return 'EDIT'; + case 'dialog_export': + return 'EXPORT'; + case 'dialog_import': + return 'IMPORT'; + case 'dialog_close': + return 'CLOSE'; + case 'dialog_clear': + return 'CLEAR'; + case 'dialog_create': + return 'CREATE'; + case 'dialog_delete': + return 'DELETE'; + case 'dialog_cancel': + return 'CANCEL'; + case 'dialog_select': + return 'SELECT'; + case 'dialog_stash': + return 'STASH'; + case 'dialog_search': + return 'SEARCH'; + case 'dialog_exit': + return 'EXIT'; + case 'dialog_share': + return 'SHARE'; + case 'dialog_pop': + return 'POP'; + case 'dialog_save': + return 'SAVE'; + case 'dialog_set': + return 'SET'; + case 'dialog_browse': + return 'BROWSE'; + case 'dialog_channel': + return 'CHANNEL'; + case 'dialog_directory': + return 'DIRECTORY'; + case 'dialog_crop': + return 'CROP'; + case 'dialog_connect': + return 'CONNECT'; + case 'dialog_append': + return 'APPEND'; + case 'dialog_record': + return 'RECORD'; + case 'dialog_manage': + return 'MANAGE'; + case 'dialog_stop': + return 'STOP'; + case 'dialog_done': + return 'DONE'; + case 'reset': + return 'Reset'; + case 'dialog_launch_ankidroid': + return 'LAUNCH ANKIDROID'; + case 'media_item_delete_confirmation': + return 'This will clear this item from history. Are you sure you want to do this?'; + case 'dictionaries_delete_confirmation': + return 'Deleting a dictionary will also clear all dictionary results from history. Are you sure you want to do this?'; + case 'mappings_delete_confirmation': + return 'This profile will be deleted. Are you sure you want to do this?'; + case 'catalog_delete_confirmation': + return 'This catalog will be deleted. Are you sure you want to do this?'; + case 'dictionaries_deleting_data': + return 'Deleting dictionary data...'; + case 'dictionaries_menu_empty': + return 'Import a dictionary for use'; + case 'options_theme_light': + return 'Use light theme'; + case 'options_theme_dark': + return 'Use dark theme'; + case 'options_incognito_on': + return 'Turn on incognito mode'; + case 'options_incognito_off': + return 'Turn off incognito mode'; + case 'options_dictionaries': + return 'Manage dictionaries'; + case 'options_profiles': + return 'Export profiles'; + case 'options_enhancements': + return 'User enhancements'; + case 'options_language': + return 'Language settings'; + case 'options_github': + return 'View repository on GitHub'; + case 'options_attribution': + return 'Licenses and attribution'; + case 'options_copy': + return 'Copy'; + case 'options_collapse': + return 'Collapse'; + case 'options_expand': + return 'Expand'; + case 'options_delete': + return 'Delete'; + case 'options_show': + return 'Show'; + case 'options_hide': + return 'Hide'; + case 'options_edit': + return 'Edit'; + case 'info_empty_home_tab': + return 'History is empty'; + case 'delete_in_progress': + return 'Delete in progress'; + case 'import_format': + return 'Import format'; + case 'import_in_progress': + return 'Import in progress'; + case 'import_start': + return 'Preparing for import...'; + case 'import_clean': + return 'Cleaning working space...'; + case 'import_extract': + return 'Extracting files...'; + case 'import_name': + return ({required Object name}) => 'Importing 『${name}』...'; + case 'import_entries': + return 'Processing entries...'; + case 'import_found_entry': + return ({required Object count}) => 'Found ${count} entries...'; + case 'import_found_tag': + return ({required Object count}) => 'Found ${count} tags...'; + case 'import_found_frequency': + return ({required Object count}) => + 'Found ${count} frequency entries...'; + case 'import_found_pitch': + return ({required Object count}) => + 'Found ${count} pitch accent entries...'; + case 'import_write_entry': + return ({required Object count, required Object total}) => + 'Writing entries:\n${count} / ${total}'; + case 'import_write_tag': + return ({required Object count, required Object total}) => + 'Writing tags:\n${count} / ${total}'; + case 'import_write_frequency': + return ({required Object count, required Object total}) => + 'Writing frequency entries:\n${count} / ${total}'; + case 'import_write_pitch': + return ({required Object count, required Object total}) => + 'Writing pitch accent entries:\n${count} / ${total}'; + case 'import_failed': + return 'Dictionary import failed.'; + case 'import_complete': + return 'Dictionary import complete.'; + case 'import_duplicate': + return ({required Object name}) => + 'A dictionary with the name『${name}』is already imported.'; + case 'dialog_title_dictionary_clear': + return 'Clear all dictionaries?'; + case 'dialog_content_dictionary_clear': + return 'Wiping the dictionary database will also clear all search results in history.'; + case 'dialog_title_dictionary_delete': + return ({required Object name}) => 'Delete 『${name}』?'; + case 'dialog_content_dictionary_delete': + return 'Deleting a single dictionary may take longer than clearing the entire dictionary database. This will also clear all search results in history.'; + case 'delete_dictionary_data': + return 'Clearing all dictionary data...'; + case 'dictionary_tag': + return ({required Object name}) => 'Imported from ${name}'; + case 'legalese': + return 'A full-featured immersion language learning suite for mobile.\n\nOriginally built for the Japanese language learning community by Leo Rafael Orpilla. Logo by suzy and Aaron Marbella.\n\njidoujisho is free and open source software. See the project repository for a comprehensive list of other licenses and attribution notices. Enjoying the application? Help out by providing feedback, making a donation, reporting issues or contributing improvements on GitHub.'; + case 'same_name_dictionary_found': + return 'Dictionary with same name found.'; + case 'import_file_extension_invalid': + return ({required Object extensions}) => + 'This format expects files with the following extensions: ${extensions}'; + case 'field_label_empty': + return 'Empty'; + case 'model_to_map': + return 'Card type to use for new profile'; + case 'mapping_name': + return 'Profile name'; + case 'mapping_name_hint': + return 'Name to assign to profile'; + case 'error_profile_name': + return 'Invalid profile name'; + case 'error_profile_name_content': + return 'A profile with this name already exists or is not valid and cannot be saved.'; + case 'error_standard_profile_name': + return 'Invalid profile name'; + case 'error_standard_profile_name_content': + return 'Cannot rename the standard profile.'; + case 'error_ankidroid_api': + return 'AnkiDroid error'; + case 'error_ankidroid_api_content': + return 'There was an issue communicating with AnkiDroid.\n\nEnsure that the AnkiDroid background service is active and all relevant app permissions are granted in order to continue.'; + case 'info_standard_model': + return 'Standard card type added'; + case 'info_standard_model_content': + return '『jidoujisho Kinomoto』 has been added to AnkiDroid as a new card type.\n\nSetups making use of a different card type or field order may be used by adding a new export profile.'; + case 'error_model_missing': + return 'Missing card type'; + case 'error_model_missing_content': + return 'The corresponding card type of the currently selected profile is missing.\n\nThe profile will be deleted, and the standard profile has now been selected in its place.'; + case 'error_model_changed': + return 'Card type changed'; + case 'error_model_changed_content': + return 'The number of fields of the card type corresponding to the selected profile has changed.\n\nThe fields of the currently selected profile have been reset and will require reconfiguration.'; + case 'creator_exporting_as': + return 'Creating card with profile'; + case 'creator_exporting_as_fields_editing': + return 'Editing fields for profile'; + case 'creator_exporting_as_enhancements_editing': + return 'Editing enhancements for profile'; + case 'creator_export_card': + return 'Create Card'; + case 'info_enhancements': + return 'Enhancements enable the automation of field editing prior to card creation. Pick a slot on the right of a field to allow use of an enhancement. Up to five right slots may be utilised for each field. The enhancement in the left slot of a field will be automatically applied in instant card creation or upon launch of the Card Creator.'; + case 'info_actions': + return 'Quick actions allow for instant card creation and other automations to be used on dictionary search results. Actions can be assigned via the slots below. Up to six slots may be utilised.'; + case 'no_more_available_enhancements': + return 'No more available enhancements for this field'; + case 'no_more_available_quick_actions': + return 'No more available quick actions'; + case 'assign_auto_enhancement': + return 'Assign Auto Enhancement'; + case 'assign_manual_enhancement': + return 'Assign Manual Enhancement'; + case 'remove_enhancement': + return 'Remove Enhancement'; + case 'copy_of_mapping': + return ({required Object name}) => 'Copy of ${name}'; + case 'enter_search_term': + return 'Enter a search term...'; + case 'searching_for': + return ({required Object searchTerm}) => + 'Searching for 『${searchTerm}』...'; + case 'no_search_results': + return 'No search results found.'; + case 'edit_actions': + return 'Edit Dictionary Quick Actions'; + case 'remove_action': + return 'Remove Action'; + case 'assign_action': + return 'Assign Action'; + case 'dictionary_import_tag': + return ({required Object name}) => 'Imported from ${name}'; + case 'stash_added_single': + return ({required Object term}) => + '『${term}』has been added to the Stash.'; + case 'stash_added_multiple': + return 'Multiple items have been added to the Stash.'; + case 'stash_clear_single': + return ({required Object term}) => + '『${term}』has been removed from the Stash.'; + case 'stash_clear_title': + return 'Clear Stash'; + case 'stash_clear_description': + return 'All contents will be cleared. Are you sure?'; + case 'stash_placeholder': + return 'No items in the Stash'; + case 'stash_nothing_to_pop': + return 'No items to be popped from the Stash.'; + case 'no_sentences_found': + return 'No sentences found'; + case 'failed_online_service': + return 'Failed to communicate with online service'; + case 'search_label_before': + return 'Show all '; + case 'search_label_middle': + return 'out of '; + case 'search_label_after': + return 'search results found for'; + case 'clear_dictionary_title': + return 'Clear Dictionary Result History'; + case 'clear_dictionary_description': + return 'This will clear all dictionary results from history. Are you sure?'; + case 'clear_search_title': + return 'Clear Search History'; + case 'clear_search_description': + return 'This will clear all search terms for this history. Are you sure?'; + case 'clear_creator_title': + return 'Clear Creator'; + case 'clear_creator_description': + return 'This will clear all fields. Are you sure?'; + case 'copied_to_clipboard': + return 'Copied to clipboard.'; + case 'no_text': + return 'No text.'; + case 'info_fields': + return 'Fields are pre-filled based on the term selected on instant export or prior to opening the Card Creator. In order to include a field for card export, it must be enabled below as well as mapped in the current selected export profile. Enabled fields may also be collapsed below in order to reduce clutter during editing. Use the Clear button on the top-right of the Card Creator in order to wipe these hidden fields quickly when manually editing a card.'; + case 'edit_fields': + return 'Edit and Reorder Fields'; + case 'remove_field': + return 'Remove Field'; + case 'add_field': + return 'Assign Field'; + case 'add_field_hint': + return 'Assign a field to this row'; + case 'no_more_available_fields': + return 'No more available fields'; + case 'hidden_fields': + return 'Additional fields'; + case 'field_fallback_used': + return ({required Object field, required Object secondField}) => + 'The ${field} field used ${secondField} as its fallback search term.'; + case 'no_text_to_search': + return 'No text to search.'; + case 'image_search_label_before': + return 'Selecting image '; + case 'image_search_label_middle': + return 'out of '; + case 'image_search_label_after': + return 'found for'; + case 'image_search_label_none_middle': + return 'no image '; + case 'image_search_label_none_before': + return 'Selecting '; + case 'preparing_instant_export': + return 'Preparing card for export...'; + case 'processing_in_progress': + return 'Preparing images'; + case 'searching_in_progress': + return 'Searching for '; + case 'audio_unavailable': + return 'No audio could be found.'; + case 'no_audio_enhancements': + return 'No audio enhancements are assigned.'; + case 'card_exported': + return ({required Object deck}) => 'Card exported to 『${deck}』.'; + case 'info_incognito_on': + return 'Incognito mode on. Dictionary, media and search history will not be tracked.'; + case 'info_incognito_off': + return 'Incognito mode off. Dictionary, media and search history will be tracked.'; + case 'exit_media_title': + return 'Exit Media'; + case 'exit_media_description': + return 'This will return you to the main menu. Are you sure?'; + case 'unimplemented_source': + return 'Unimplemented source'; + case 'clear_browser_title': + return 'Clear Browser Data'; + case 'clear_browser_description': + return 'This will clear all browsing data used in media sources that use web content. Are you sure?'; + case 'ttu_no_books_added': + return 'No books added to ッツ Ebook Reader'; + case 'local_media_directory_empty': + return 'Directory has no folders or video'; + case 'pick_video_file': + return 'Pick Video File'; + case 'pin_player_bottom_bar': + return 'Pin Player Bottom Bar'; + case 'navigate_up_one_directory_level': + return 'Navigate Up One Directory Level'; + case 'play': + return 'Play'; + case 'pause': + return 'Pause'; + case 'record': + return 'Record'; + case 'stop': + return 'Stop'; + case 'replay': + return 'Replay'; + case 'audio_subtitles': + return 'Audio/Subtitles'; + case 'player_option_shadowing': + return 'Shadowing Mode'; + case 'player_option_change_mode': + return 'Change Playback Mode'; + case 'player_option_listening_comprehension': + return 'Listening Comprehension Mode'; + case 'player_option_drag_to_select': + return 'Use Drag to Select Subtitle Selection'; + case 'player_option_tap_to_select': + return 'Use Tap to Select Subtitle Selection'; + case 'player_option_dictionary_menu': + return 'Select Active Dictionary Source'; + case 'player_option_cast_video': + return 'Cast to Display Device'; + case 'player_option_share_subtitle': + return 'Share Current Subtitle'; + case 'player_option_export': + return 'Create Card from Context'; + case 'player_option_audio': + return 'Audio'; + case 'player_option_subtitle': + return 'Subtitle'; + case 'player_option_subtitle_external': + return 'External'; + case 'player_option_subtitle_none': + return 'None'; + case 'player_option_select_subtitle': + return 'Select Subtitle Track'; + case 'player_option_select_audio': + return 'Select Audio Track'; + case 'player_option_text_filter': + return 'Use Regular Expression Filter'; + case 'player_option_blur_preferences': + return 'Blur Widget Preferences'; + case 'player_option_blur_use': + return 'Use Blur Widget'; + case 'player_option_blur_radius': + return 'Blur radius'; + case 'player_option_blur_options': + return 'Set Blur Widget Color and Bluriness'; + case 'player_option_blur_reset': + return 'Reset Blur Widget Size and Position'; + case 'player_align_subtitle_transcript': + return 'Align Subtitle with Transcript'; + case 'player_option_subtitle_appearance': + return 'Subtitle Timing and Appearance'; + case 'player_option_load_subtitles': + return 'Load External Subtitles'; + case 'player_option_subtitle_delay': + return 'Subtitle delay'; + case 'player_option_audio_allowance': + return 'Audio allowance'; + case 'player_option_font_name': + return 'Subtitle font name'; + case 'player_option_font_color': + return 'Subtitle font color'; + case 'player_option_font_weight': + return 'Subtitle font weight'; + case 'player_option_font_size': + return 'Subtitle font size'; + case 'player_option_outline_size': + return 'Subtitle outline size'; + case 'player_option_regex_filter': + return 'Regular expression filter'; + case 'player_option_subtitle_background_opacity': + return 'Subtitle background opacity'; + case 'player_option_subtitle_background_blur_radius': + return 'Subtitle background blur radius'; + case 'player_option_outline_width': + return 'Subtitle outline width'; + case 'player_option_subtitle_always_above_bottom_bar': + return 'Always show subtitle above bottom bar area'; + case 'player_subtitles_transcript_empty': + return 'Transcript is empty.'; + case 'player_prepare_export': + return 'Preparing card...'; + case 'player_change_player_orientation': + return 'Change Player Orientation'; + case 'no_current_media': + return 'Play or refresh media for lyrics'; + case 'lyrics_permission_required': + return 'Required permission not granted'; + case 'no_lyrics_found': + return 'No lyrics found'; + case 'trending': + return 'Trending'; + case 'caption_filter': + return 'Filter Closed Captions'; + case 'captions_query': + return 'Querying for captions'; + case 'captions_target': + return 'Target language'; + case 'captions_app': + return 'App language'; + case 'captions_other': + return 'Other language'; + case 'captions_closed': + return 'Closed captioning'; + case 'captions_auto': + return 'Automatic captioning'; + case 'captions_unavailable': + return 'No captioning'; + case 'captions_error': + return 'Error while querying captions'; + case 'change_quality': + return 'Change Quality'; + case 'closed_captions_query': + return 'Querying for captions'; + case 'closed_captions_target': + return 'Target language captions'; + case 'closed_captions_app': + return 'App language captions'; + case 'closed_captions_other': + return 'Other language captions'; + case 'closed_captions_unavailable': + return 'No captions'; + case 'closed_captions_error': + return 'Error while querying captions'; + case 'stream_url': + return 'Stream URL'; + case 'default_option': + return 'Default'; + case 'paste': + return 'Paste'; + case 'lyrics_title': + return 'Title'; + case 'lyrics_artist': + return 'Artist'; + case 'set_media': + return 'Set Media'; + case 'no_recordings_found': + return 'No recordings found'; + case 'wrap_image_audio': + return 'Include image/audio HTML tags on export'; + case 'server_address': + return 'Server Address'; + case 'no_active_connection': + return 'No active connection'; + case 'failed_server_connection': + return 'Failed to connect to server'; + case 'no_text_received': + return 'No text received'; + case 'text_segmentation': + return 'Text Segmentation'; + case 'connect_disconnect': + return 'Connect/Disconnect'; + case 'clear_text_title': + return 'Clear Text'; + case 'clear_text_description': + return 'This will clear all received text. Are you sure?'; + case 'close_connection_title': + return 'Close Connection'; + case 'close_connection_description': + return 'This will end the WebSocket connection and clear all received text. Are you sure?'; + case 'use_slow_import': + return 'Slow import (use if failing)'; + case 'settings': + return 'Settings'; + case 'manager': + return 'Manager'; + case 'volume_button_page_turning': + return 'Volume button page turning'; + case 'invert_volume_buttons': + return 'Invert volume buttons'; + case 'volume_button_turning_speed': + return 'Continuous scrolling speed'; + case 'extend_page_beyond_navbar': + return 'Extend page beyond navigation bar'; + case 'tweaks': + return 'Tweaks'; + case 'increase': + return 'Increase'; + case 'decrease': + return 'Decrease'; + case 'unit_milliseconds': + return 'ms'; + case 'unit_pixels': + return 'px'; + case 'dictionary_settings': + return 'Dictionary Settings'; + case 'auto_search': + return 'Auto search'; + case 'auto_search_debounce_delay': + return 'Auto search debounce delay'; + case 'dictionary_font_size': + return 'Dictionary font size'; + case 'close_on_export': + return 'Close on Export'; + case 'close_on_export_on': + return 'The Card Creator will now automatically close upon card export.'; + case 'close_on_export_off': + return 'The Card Creator will no longer close upon card export.'; + case 'export_profile_empty': + return 'Your export profile has no set fields and requires configuration.'; + case 'error_export_media_ankidroid': + return 'There was an error in exporting media to AnkiDroid.'; + case 'error_add_note': + return 'There was an error in adding a note to AnkiDroid.'; + case 'first_time_setup': + return 'First-Time Setup'; + case 'first_time_setup_description': + return 'Welcome to jidoujisho! Set your target language and a default profile will be tailored for you. You can change this later at anytime.'; + case 'maximum_entries': + return 'Maximum dictionary entry query limit'; + case 'maximum_terms': + return 'Maximum dictionary headwords in result'; + case 'use_br_tags': + return 'Use line break tag instead of newline on export'; + case 'prepend_dictionary_names': + return 'Prepend dictionary name in meaning'; + case 'highlight_on_tap': + return 'Highlight text on tap'; + case 'no_audio_file': + return 'No audio file to save.'; + case 'storage_permissions': + return 'Please grant the following permissions for exporting to AnkiDroid.'; + case 'stream': + return 'Stream'; + case 'network_subtitles_warning': + return 'Embedded subtitles are unsupported for network streams.'; + case 'accessibility': + return 'Permission is required to capture text from accessibility events.'; + case 'comments': + return 'Comments'; + case 'replies': + return 'Replies'; + case 'no_comments_queried': + return 'No comments queried'; + case 'no_text_in_clipboard': + return 'No text to display'; + case 'file_downloaded': + return ({required Object name}) => 'File downloaded: ${name}'; + case 'cfhange_sort_order': + return 'Change Sort Order'; + case 'login': + return 'Login'; + case 'send': + return 'Send'; + case 'no_messages': + return 'Start a chat'; + case 'enter_message': + return 'Enter message...'; + case 'clear_message_title': + return 'Clear Messages'; + case 'clear_message_description': + return 'This will clear all messages and start a new chat. Are you sure?'; + case 'error_chatgpt_response': + return 'Request failed or rate-limited. Try again shortly or check your usage limits.'; + case 'pick_file': + return 'Pick File'; + case 'open_url': + return 'Open URL'; + case 'catalogs': + return 'Catalogs'; + case 'name': + return 'Name'; + case 'url': + return 'URL'; + case 'duplicate_catalog': + return 'A catalog with this URL already exists.'; + case 'no_catalogs_listed': + return 'No catalogs listed'; + case 'go_back': + return 'Go Back'; + case 'invalid_mokuro_file': + return 'File is not a Mokuro generated HTML file.'; + case 'create_catalog': + return 'Create Catalog'; + case 'adapt_ttu_theme': + return 'Adapt dictionary popup to theme'; + case 'sentence_picker': + return 'Sentence Picker'; + case 'field_locked': + return ({required Object field}) => + '${field} locked and will not clear on export while Creator is active.'; + case 'field_unlocked': + return ({required Object field}) => + '${field} unlocked and will clear on export.'; + case 'field_lock': + return 'Lock Field'; + case 'field_unlock': + return 'Unlock Field'; + case 'use_dark_theme': + return 'Use dark theme'; + case 'stretch_to_fill_screen': + return 'Stretch to Fill Screen'; + case 'processing_embedded_subtitles': + return 'Embedded subtitles are processing. Try again later.'; + case 'transcript_playback_mode': + return 'Transcript Playback Mode'; + case 'toggle_transcript_background': + return 'Toggle Transcript Background'; + case 'seek': + return 'Seek'; + case 'saved_tags': + return 'Tags saved.'; + case 'structured_content_first': + return ({required Object i}) => + '${i} definitions are unsupported and were omitted.'; + case 'structured_content_second': + return 'Consider a non-structured content version of this dictionary.'; + case 'missing_api_key': + return 'API key not provided'; + case 'chatgpt_error': + return 'There was an error in getting a response from ChatGPT.'; + case 'api_key': + return 'API Key'; + case 'subtitle_delay_set': + return ({required Object ms}) => 'Subtitle delay set to ${ms} ms.'; + case 'cancel': + return 'Cancel'; + case 'server_port_in_use': + return 'Local server port already in use'; + case 'google_fonts': + return 'Google Fonts'; + case 'video_show': + return 'Show video'; + case 'video_hide': + return 'Hide video'; + case 'subtitle_timing_show': + return 'Show subtitle timings'; + case 'subtitle_timing_hide': + return 'Hide subtitle timings'; + case 'find_next': + return 'Find Next'; + case 'find_previous': + return 'Find Previous'; + case 'shadowing_mode': + return 'Shadowing Mode'; + case 'display_settings': + return 'Display Settings'; + case 'cloze': + return 'Cloze'; + case 'info_standard_update': + return 'New standard profile card type'; + case 'info_standard_update_content': + return 'The standard profile now uses the『jidoujisho Kinomoto』 card type.\n\nYour legacy standard profile remains available for backwards compatibility.'; + case 'retrying_in.seconds': + return ({required num n}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))( + n, + one: 'Retrying in ${n} second...', + other: 'Retrying in ${n} seconds...', + ); + case 'view_replies.reply': + return ({required num n}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))( + n, + one: 'SHOW ${n} REPLY', + other: 'SHOW ${n} REPLIES', + ); + case 'manage_duplicate_checks': + return 'Manage Duplicate Checks'; + case 'playback_normal': + return 'Normal Playback Mode'; + case 'playback_condensed': + return 'Condensed Playback Mode'; + case 'playback_auto_pause': + return 'Subtitle Pause Playback Mode'; + case 'player_hardware_acceleration': + return 'Hardware acceleration'; + case 'player_use_opensles': + return 'OpenSL ES audio'; + case 'go_forward': + return 'Go Forward'; + case 'browse': + return 'Browse'; + case 'bookmark': + return 'Bookmark'; + case 'add_bookmark': + return 'Add Bookmark'; + case 'add_to_reading_list': + return 'Add To Reading List'; + case 'reading_list_empty': + return 'Reading list is empty'; + case 'reading_list_add_toast': + return 'Added to reading list.'; + case 'reading_list_remove_toast': + return 'Removed from the reading list.'; + case 'ad_block_hosts': + return 'Ad-block hosts'; + case 'error_parsing_hosts_file': + return 'Error parsing hosts file.'; + case 'double_tap_seek_duration': + return 'Double tap seek duration'; + case 'player_background_play': + return 'Background play'; + case 'loaded_from_cache': + return 'Loaded from web archive cache.'; + case 'player_show_subtitle_in_notification': + return 'Show subtitles in media notification'; + case 'subtitles_processing': + return 'Subtitles are processing...'; + case 'video_unavailable': + return 'Video Unavailable'; + case 'video_unavailable_content': + return 'Cannot fetch streams. There may be restrictions in place that prevent watching this video.'; + case 'video_file_error': + return 'Cannot Load File'; + case 'video_file_error_content': + return 'Unable to load the video file. Please ensure this file exists and is located in a directory accessible by the application.'; + default: + return null; + } + } } diff --git a/yuuna/lib/src/models/app_model.dart b/yuuna/lib/src/models/app_model.dart index 1c28caa2..b4fcc9a0 100644 --- a/yuuna/lib/src/models/app_model.dart +++ b/yuuna/lib/src/models/app_model.dart @@ -3227,11 +3227,15 @@ class AppModel with ChangeNotifier { double fontSize = _preferences.get('font_size', defaultValue: 20.0); String fontName = _preferences .get('font_name/${targetLanguage.languageCode}', defaultValue: ''); + int fontColor = _preferences.get('font_color', defaultValue: 0xff00EE); + String fontWeight = _preferences.get('font_weight', defaultValue: 'Normal'); String regexFilter = _preferences.get('regex_filter', defaultValue: ''); double subtitleBackgroundOpacity = _preferences.get('subtitle_background_opacity', defaultValue: 0.0); double subtitleOutlineWidth = _preferences.get('subtitle_outline_width', defaultValue: 3.0); + int subtitleOutlineColor = + _preferences.get('subtitle_outline_color', defaultValue: 0xffffff); double subtitleBackgroundBlurRadius = _preferences.get('subtitle_background_blur_radius', defaultValue: 0.0); bool alwaysAboveBottomBar = @@ -3244,8 +3248,11 @@ class AppModel with ChangeNotifier { subtitleBackgroundBlurRadius: subtitleBackgroundBlurRadius, fontSize: fontSize, fontName: fontName, + fontColor: fontColor, + fontWeight: fontWeight, regexFilter: regexFilter, subtitleOutlineWidth: subtitleOutlineWidth, + subtitleOutlineColor: subtitleOutlineColor, alwaysAboveBottomBar: alwaysAboveBottomBar, ); } @@ -3255,6 +3262,8 @@ class AppModel with ChangeNotifier { _preferences.put('audio_allowance', options.audioAllowance); _preferences.put('subtitle_delay', options.subtitleDelay); _preferences.put('font_size', options.fontSize); + _preferences.put('font_color', options.fontColor); + _preferences.put('font_weight', options.fontWeight); _preferences.put( 'font_name/${targetLanguage.languageCode}', options.fontName); _preferences.put('regex_filter', options.regexFilter); diff --git a/yuuna/lib/src/pages/implementations/player_source_page.dart b/yuuna/lib/src/pages/implementations/player_source_page.dart index e1c0f7c6..ea4153bb 100644 --- a/yuuna/lib/src/pages/implementations/player_source_page.dart +++ b/yuuna/lib/src/pages/implementations/player_source_page.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_vlc_player/flutter_vlc_player.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:google_fonts/google_fonts.dart'; +// import 'package:google_fonts/google_fonts.dart'; import 'package:multi_value_listenable_builder/multi_value_listenable_builder.dart'; import 'package:receive_intent/receive_intent.dart'; import 'package:share_plus/share_plus.dart'; @@ -65,6 +65,7 @@ class _PlayerSourcePageState extends BaseSourcePageState final ValueNotifier _bufferingNotifier = ValueNotifier(false); final ValueNotifier _isMenuHidden = ValueNotifier(false); + final ValueNotifier _isMenuShownPermanent = ValueNotifier(false); late final ValueNotifier _currentSubtitle; @@ -1180,16 +1181,33 @@ class _PlayerSourcePageState extends BaseSourcePageState Widget buildMenuArea() { return Align( alignment: Alignment.topCenter, - child: ValueListenableBuilder( - valueListenable: _isMenuHidden, - builder: (context, value, _) { + child: MultiValueListenableBuilder( + valueListenables: [ + _isMenuHidden, + _isMenuShownPermanent, + ], + builder: (context, values, _) { return AnimatedOpacity( - opacity: value ? 0.0 : 1.0, + opacity: values[1] + ? 1.0 + : values[0] + ? 0.0 + : 1.0, duration: const Duration(milliseconds: 200), child: buildMenuContent(), ); }, ), + // ValueListenableBuilder( + // valueListenable: _isMenuHidden, + // builder: (context, value, _) { + // return AnimatedOpacity( + // opacity: value ? 0.0 : 1.0, + // duration: const Duration(milliseconds: 200), + // child: buildMenuContent(), + // ); + // }, + // ), ); } @@ -1207,6 +1225,7 @@ class _PlayerSourcePageState extends BaseSourcePageState child: Row( children: [ const Space.small(), + buildPinButton(), buildPlayButton(), buildDurationAndPosition(), buildSlider(), @@ -1222,6 +1241,33 @@ class _PlayerSourcePageState extends BaseSourcePageState ); } + /// This shows the pin button in the bottomleft of the screen. + Widget buildPinButton() { + return Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: _isMenuShownPermanent.value + ? Icons.push_pin + : Icons.push_pin_outlined, + tooltip: t.pin_player_bottom_bar, + onTap: () async { + Wakelock.enable(); + _menuHideTimer?.cancel(); + _isMenuShownPermanent.value = !_isMenuShownPermanent.value; + + if (!_isMenuShownPermanent.value) { + _menuHideTimer = Timer(const Duration(seconds: 3), () { + if (_playingNotifier.value) { + _isMenuHidden.value = true; + } + }); + } + }, + ), + ); + } + /// This shows the play/pause button in the bottomleft of the screen. Widget buildPlayButton() { return MultiValueListenableBuilder( @@ -2339,35 +2385,57 @@ class _PlayerSourcePageState extends BaseSourcePageState Paint get subtitlePaintStyle => Paint() ..style = PaintingStyle.stroke ..strokeWidth = _subtitleOptionsNotifier.value.subtitleOutlineWidth - ..color = Colors.black.withOpacity( - _subtitleOptionsNotifier.value.subtitleOutlineWidth == 0 ? 0 : 0.75); + ..color = Color(_subtitleOptionsNotifier.value.subtitleOutlineColor) + .withOpacity(_subtitleOptionsNotifier.value.subtitleOutlineWidth == 0 + ? 0 + : 0.75); /// Subtitle outline text style. - /// - TextStyle get subtitleOutlineStyle => - _subtitleOptionsNotifier.value.fontName.trim().isEmpty - ? TextStyle( - fontSize: _subtitleOptionsNotifier.value.fontSize, - foreground: subtitlePaintStyle, - ) - : GoogleFonts.getFont( - _subtitleOptionsNotifier.value.fontName, - fontSize: _subtitleOptionsNotifier.value.fontSize, - foreground: subtitlePaintStyle, - ); + TextStyle get subtitleOutlineStyle => TextStyle( + fontFamily: _subtitleOptionsNotifier.value.fontName, + fontSize: _subtitleOptionsNotifier.value.fontSize, + fontWeight: _subtitleOptionsNotifier.value.fontWeight == 'Thin' + ? FontWeight.w300 + : _subtitleOptionsNotifier.value.fontWeight == 'Normal' + ? FontWeight.normal + : FontWeight.bold, + foreground: subtitlePaintStyle, + ); + // TextStyle get subtitleOutlineStyle => + // _subtitleOptionsNotifier.value.fontName.trim().isEmpty + // ? TextStyle( + // fontSize: _subtitleOptionsNotifier.value.fontSize, + // foreground: subtitlePaintStyle, + // ) + // : GoogleFonts.getFont( + + // _subtitleOptionsNotifier.value.fontName, + // fontSize: _subtitleOptionsNotifier.value.fontSize, + // foreground: subtitlePaintStyle, + // ); /// Subtitle text style. - TextStyle get subtitleTextStyle => - _subtitleOptionsNotifier.value.fontName.trim().isEmpty - ? TextStyle( - fontSize: _subtitleOptionsNotifier.value.fontSize, - color: Colors.white, - ) - : GoogleFonts.getFont( - _subtitleOptionsNotifier.value.fontName, - fontSize: _subtitleOptionsNotifier.value.fontSize, - color: Colors.white, - ); + TextStyle get subtitleTextStyle => TextStyle( + fontFamily: _subtitleOptionsNotifier.value.fontName, + fontSize: _subtitleOptionsNotifier.value.fontSize, + fontWeight: _subtitleOptionsNotifier.value.fontWeight == 'Thin' + ? FontWeight.w100 + : _subtitleOptionsNotifier.value.fontWeight == 'Normal' + ? FontWeight.normal + : FontWeight.bold, + color: Color(_subtitleOptionsNotifier.value.fontColor), + ); + // TextStyle get subtitleTextStyle => + // _subtitleOptionsNotifier.value.fontName.trim().isEmpty + // ? TextStyle( + // fontSize: _subtitleOptionsNotifier.value.fontSize, + // color: Color(_subtitleOptionsNotifier.value.fontColor), + // ) + // : GoogleFonts.getFont( + // _subtitleOptionsNotifier.value.fontName, + // fontSize: _subtitleOptionsNotifier.value.fontSize, + // color: Color(_subtitleOptionsNotifier.value.fontColor), + // ); /// This is used to set the search term upon pressing on a character /// or selecting text. diff --git a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart index cd582cec..8ecd8140 100644 --- a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter/services.dart'; +// import 'package:google_fonts/google_fonts.dart'; import 'package:spaces/spaces.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:yuuna/language.dart'; +// import 'package:url_launcher/url_launcher_string.dart'; +// import 'package:yuuna/language.dart'; import 'package:yuuna/pages.dart'; import 'package:yuuna/utils.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'dart:io'; /// The content of the dialog when editing [SubtitleOptions]. class SubtitleOptionsDialogPage extends BasePage { @@ -29,18 +33,27 @@ class _SubtitleOptionsDialogPage late final TextEditingController _delayController; late final TextEditingController _fontSizeController; late final TextEditingController _fontNameController; + late final TextEditingController _fontColorController; + late final TextEditingController _outLineColorController; late final TextEditingController _regexFilterController; late final TextEditingController _opacityController; late final TextEditingController _widthController; late final TextEditingController _blurController; late ValueNotifier _aboveBottomBarNotifier; + Color fontColor = Colors.white; + Color outLineColor = Colors.white; + List fontWeights = ['Thin', 'Normal', 'Bold']; + int fontWeightIdx = 1; @override void initState() { super.initState(); _options = widget.notifier.value; + fontWeightIdx = fontWeights.indexOf(_options.fontWeight); + fontColor = Color(_options.fontColor); + outLineColor = Color(_options.subtitleOutlineColor); _allowanceController = TextEditingController(text: _options.audioAllowance.toString()); _delayController = @@ -48,6 +61,8 @@ class _SubtitleOptionsDialogPage _fontSizeController = TextEditingController(text: _options.fontSize.toString()); _fontNameController = TextEditingController(text: _options.fontName.trim()); + _fontColorController = TextEditingController(text: 'Font Color'); + _outLineColorController = TextEditingController(text: 'Outline Color'); _regexFilterController = TextEditingController(text: _options.regexFilter.trim()); _opacityController = TextEditingController( @@ -94,6 +109,7 @@ class _SubtitleOptionsDialogPage width: MediaQuery.of(context).size.width * (1 / 3), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _delayController, @@ -293,11 +309,12 @@ class _SubtitleOptionsDialogPage tooltip: t.google_fonts, onTap: () async { /// Language Customizable - if (appModel.targetLanguage is JapaneseLanguage) { - launchUrlString( - 'https://fonts.google.com/?subset=japanese'); - } - launchUrlString('https://fonts.google.com/'); + // if (appModel.targetLanguage is JapaneseLanguage) { + // launchUrlString( + // 'https://fonts.google.com/?subset=japanese'); + // } + // launchUrlString('https://fonts.google.com/'); + pickFontFile(); }, icon: Icons.font_download, ), @@ -315,6 +332,94 @@ class _SubtitleOptionsDialogPage ), ), ), + TextField( + controller: _fontColorController, + style: TextStyle(color: fontColor), + readOnly: true, + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: t.player_option_font_color, + suffixIcon: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + JidoujishoIconButton( + size: 18, + tooltip: t.google_fonts, + onTap: () async { + showColorPicker('Font'); + }, + icon: Icons.color_lens, + ), + JidoujishoIconButton( + size: 18, + tooltip: t.reset, + onTap: () async { + _fontColorController.text = ''; + FocusScope.of(context).unfocus(); + }, + icon: Icons.undo, + ), + const SizedBox(width: 8), + ], + ), + ), + ), + const Space.small(), + Padding( + padding: Spacing.of(context).insets.onlyTop.small, + child: Text( + t.player_option_font_weight, + style: TextStyle( + fontSize: 12, + color: theme.hintColor, + ), + ), + ), + JidoujishoDropdown( + options: fontWeights, + initialOption: fontWeights[fontWeightIdx], + generateLabel: (weight) => weight, + onChanged: (weight) { + fontWeightIdx = fontWeights.indexOf(weight ?? 'Normal'); + setState(() {}); + }, + ), + Container(height: 0.45, color: Colors.black87), + const Space.small(), + TextField( + controller: _outLineColorController, + readOnly: true, + style: TextStyle(color: outLineColor), + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: t.player_option_outline_color, + suffixIcon: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + JidoujishoIconButton( + size: 18, + tooltip: t.google_fonts, + onTap: () async { + showColorPicker('Outline'); + }, + icon: Icons.color_lens, + ), + JidoujishoIconButton( + size: 18, + tooltip: t.reset, + onTap: () async { + _fontColorController.text = ''; + FocusScope.of(context).unfocus(); + }, + icon: Icons.undo, + ), + const SizedBox(width: 8), + ], + ), + ), + ), TextField( controller: _regexFilterController, keyboardType: TextInputType.text, @@ -397,11 +502,11 @@ class _SubtitleOptionsDialogPage newWidth >= 0 && newBlur >= 0) { RegExp(newRegexFilter); - try { - GoogleFonts.getFont(newFontName); - } catch (e) { - newFontName = ''; - } + // try { + // GoogleFonts.getFont(newFontName); + // } catch (e) { + // newFontName = ''; + // } SubtitleOptions subtitleOptions = appModel.subtitleOptions; @@ -410,8 +515,11 @@ class _SubtitleOptionsDialogPage subtitleOptions.regexFilter = newRegexFilter; subtitleOptions.fontName = newFontName; subtitleOptions.fontSize = newFontSize; + subtitleOptions.fontColor = fontColor.value; + subtitleOptions.fontWeight = fontWeights[fontWeightIdx]; subtitleOptions.subtitleBackgroundOpacity = newOpacity; subtitleOptions.subtitleOutlineWidth = newWidth; + subtitleOptions.subtitleOutlineColor = outLineColor.value; subtitleOptions.subtitleBackgroundBlurRadius = newBlur; subtitleOptions.alwaysAboveBottomBar = newAlwaysAboveBottomBar; @@ -459,4 +567,53 @@ class _SubtitleOptionsDialogPage void executeSet() async { await setValues(saveOptions: false); } + + /// Pick a font file with a built-in file picker. + Future pickFontFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['ttf', 'otf'], + ); + if (result != null) { + _fontNameController.text = result.files.single.name.split('.').first; + var custom = FontLoader(_fontNameController.text); + custom.addFont(loadFont(result.files.single.path ?? '')); + await custom.load(); + return true; + } + return false; + } + + Future loadFont(String path) async { + File file = File(path); + Uint8List bytes = await file.readAsBytes(); + return ByteData.view(bytes.buffer); + } + + void showColorPicker(String target) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Pick color'), + content: SingleChildScrollView( + child: ColorPicker( + pickerColor: const Color(0xff443a49), + paletteType: PaletteType.hueWheel, + onColorChanged: (value) { + if (target == 'Font') { + setState(() { + fontColor = value; + }); + } else { + setState(() { + outLineColor = value; + }); + } + }, + ), + ), + ); + }); + } } diff --git a/yuuna/lib/src/utils/player/subtitle_options.dart b/yuuna/lib/src/utils/player/subtitle_options.dart index 0f38f92f..71b94556 100644 --- a/yuuna/lib/src/utils/player/subtitle_options.dart +++ b/yuuna/lib/src/utils/player/subtitle_options.dart @@ -6,9 +6,12 @@ class SubtitleOptions { required this.subtitleDelay, required this.fontSize, required this.fontName, + required this.fontColor, + required this.fontWeight, required this.subtitleBackgroundOpacity, required this.regexFilter, required this.subtitleOutlineWidth, + required this.subtitleOutlineColor, required this.subtitleBackgroundBlurRadius, required this.alwaysAboveBottomBar, }); @@ -25,6 +28,12 @@ class SubtitleOptions { /// Name of the font preferred for the subtitle. String fontName; + /// Font color preferred for the subtitle. + int fontColor; + + /// Font weight preferred for the subtitle. + String fontWeight; + /// Subtitle background blur radius. double subtitleBackgroundBlurRadius; @@ -34,6 +43,9 @@ class SubtitleOptions { /// Subtitle outline width. double subtitleOutlineWidth; + /// Subtitle outline color. + int subtitleOutlineColor; + /// Regex filter used for the subtitle. String regexFilter; From e7936964da112faa62d1860ad3cc6c64df0c0dbc Mon Sep 17 00:00:00 2001 From: SmartDevStar Date: Tue, 18 Jul 2023 12:52:40 -0600 Subject: [PATCH 2/7] updated for first PR --- yuuna/lib/i18n/strings.g.dart | 6 ++ .../subtitle_options_dialog_page.dart | 76 +++++++++++-------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/yuuna/lib/i18n/strings.g.dart b/yuuna/lib/i18n/strings.g.dart index 305c0725..62236158 100644 --- a/yuuna/lib/i18n/strings.g.dart +++ b/yuuna/lib/i18n/strings.g.dart @@ -617,8 +617,10 @@ class _StringsEn implements BaseTranslations { String subtitle_delay_set({required Object ms}) => 'Subtitle delay set to ${ms} ms.'; String get cancel => 'Cancel'; + String get choose_color => 'Choose'; String get server_port_in_use => 'Local server port already in use'; String get google_fonts => 'Google Fonts'; + String get pick_color => 'Pick Color'; String get video_show => 'Show video'; String get video_hide => 'Hide video'; String get subtitle_timing_show => 'Show subtitle timings'; @@ -1429,10 +1431,14 @@ extension on _StringsEn { return ({required Object ms}) => 'Subtitle delay set to ${ms} ms.'; case 'cancel': return 'Cancel'; + case 'choose_color': + return 'Choose'; case 'server_port_in_use': return 'Local server port already in use'; case 'google_fonts': return 'Google Fonts'; + case 'pick_color': + return 'Pick Color'; case 'video_show': return 'Show video'; case 'video_hide': diff --git a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart index 8ecd8140..deade6e0 100644 --- a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart @@ -34,16 +34,15 @@ class _SubtitleOptionsDialogPage late final TextEditingController _fontSizeController; late final TextEditingController _fontNameController; late final TextEditingController _fontColorController; - late final TextEditingController _outLineColorController; + late final TextEditingController _outlineColorController; late final TextEditingController _regexFilterController; late final TextEditingController _opacityController; late final TextEditingController _widthController; late final TextEditingController _blurController; late ValueNotifier _aboveBottomBarNotifier; - Color fontColor = Colors.white; - Color outLineColor = Colors.white; List fontWeights = ['Thin', 'Normal', 'Bold']; + int fontWeightIdx = 1; @override @@ -52,8 +51,6 @@ class _SubtitleOptionsDialogPage _options = widget.notifier.value; fontWeightIdx = fontWeights.indexOf(_options.fontWeight); - fontColor = Color(_options.fontColor); - outLineColor = Color(_options.subtitleOutlineColor); _allowanceController = TextEditingController(text: _options.audioAllowance.toString()); _delayController = @@ -61,8 +58,10 @@ class _SubtitleOptionsDialogPage _fontSizeController = TextEditingController(text: _options.fontSize.toString()); _fontNameController = TextEditingController(text: _options.fontName.trim()); - _fontColorController = TextEditingController(text: 'Font Color'); - _outLineColorController = TextEditingController(text: 'Outline Color'); + _fontColorController = + TextEditingController(text: '#${_options.fontColor.toRadixString(16)}'); + _outlineColorController = TextEditingController( + text: '#${_options.subtitleOutlineColor.toRadixString(16)}'); _regexFilterController = TextEditingController(text: _options.regexFilter.trim()); _opacityController = TextEditingController( @@ -334,8 +333,6 @@ class _SubtitleOptionsDialogPage ), TextField( controller: _fontColorController, - style: TextStyle(color: fontColor), - readOnly: true, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: t.player_option_font_color, @@ -345,7 +342,7 @@ class _SubtitleOptionsDialogPage children: [ JidoujishoIconButton( size: 18, - tooltip: t.google_fonts, + tooltip: t.pick_color, onTap: () async { showColorPicker('Font'); }, @@ -388,9 +385,8 @@ class _SubtitleOptionsDialogPage Container(height: 0.45, color: Colors.black87), const Space.small(), TextField( - controller: _outLineColorController, + controller: _outlineColorController, readOnly: true, - style: TextStyle(color: outLineColor), decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: t.player_option_outline_color, @@ -400,7 +396,7 @@ class _SubtitleOptionsDialogPage children: [ JidoujishoIconButton( size: 18, - tooltip: t.google_fonts, + tooltip: t.pick_color, onTap: () async { showColorPicker('Outline'); }, @@ -477,6 +473,13 @@ class _SubtitleOptionsDialogPage String fontSizeText = _fontSizeController.text; double? newFontSize = double.tryParse(fontSizeText); + String fontColorText = _fontColorController.text; + int? newFontColor = int.tryParse(fontColorText.replaceFirst('#', '0xFF')); + + String outlineColorText = _outlineColorController.text; + int? newOutlineColor = + int.tryParse(outlineColorText.replaceFirst('#', '0xFF')); + String newFontName = _fontNameController.text.trim(); String newRegexFilter = _regexFilterController.text.trim(); @@ -494,6 +497,8 @@ class _SubtitleOptionsDialogPage if (newDelay != null && newAllowance != null && newFontSize != null && + newFontColor != null && + newOutlineColor != null && newOpacity != null && newWidth != null && newBlur != null && @@ -515,11 +520,11 @@ class _SubtitleOptionsDialogPage subtitleOptions.regexFilter = newRegexFilter; subtitleOptions.fontName = newFontName; subtitleOptions.fontSize = newFontSize; - subtitleOptions.fontColor = fontColor.value; + subtitleOptions.fontColor = newFontColor; subtitleOptions.fontWeight = fontWeights[fontWeightIdx]; subtitleOptions.subtitleBackgroundOpacity = newOpacity; subtitleOptions.subtitleOutlineWidth = newWidth; - subtitleOptions.subtitleOutlineColor = outLineColor.value; + subtitleOptions.subtitleOutlineColor = newOutlineColor; subtitleOptions.subtitleBackgroundBlurRadius = newBlur; subtitleOptions.alwaysAboveBottomBar = newAlwaysAboveBottomBar; @@ -577,42 +582,53 @@ class _SubtitleOptionsDialogPage if (result != null) { _fontNameController.text = result.files.single.name.split('.').first; var custom = FontLoader(_fontNameController.text); - custom.addFont(loadFont(result.files.single.path ?? '')); + File file = File(result.files.single.path ?? ''); + Uint8List bytes = await file.readAsBytes(); + custom.addFont(Future.value(ByteData.view(bytes.buffer))); await custom.load(); return true; } return false; } - Future loadFont(String path) async { - File file = File(path); - Uint8List bytes = await file.readAsBytes(); - return ByteData.view(bytes.buffer); - } - void showColorPicker(String target) { + Color newColor = target == 'Font' + ? Color(_options.fontColor) + : Color(_options.subtitleOutlineColor); showDialog( context: context, builder: (context) { return AlertDialog( - title: const Text('Pick color'), content: SingleChildScrollView( child: ColorPicker( pickerColor: const Color(0xff443a49), paletteType: PaletteType.hueWheel, onColorChanged: (value) { + newColor = value; + }, + ), + ), + actions: [ + TextButton( + child: Text(t.choose_color), + onPressed: () { if (target == 'Font') { - setState(() { - fontColor = value; - }); + _fontColorController.text = + '#${newColor.value.toRadixString(16)}'; } else { - setState(() { - outLineColor = value; - }); + _outlineColorController.text = + '#${newColor.value.toRadixString(16)}'; } + Navigator.of(context).pop(); }, ), - ), + TextButton( + child: Text(t.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], ); }); } From cf657ee6fcbb73b4d162b81f3ddf9a2739fd67fa Mon Sep 17 00:00:00 2001 From: SmartDevStar Date: Wed, 19 Jul 2023 16:03:28 -0600 Subject: [PATCH 3/7] fixed issues for subtitle font option issues and store bottom bar keepShown option --- yuuna/lib/i18n/strings.g.dart | 3 + yuuna/lib/pages.dart | 1 + yuuna/lib/src/models/app_model.dart | 24 ++++ .../implementations/player_source_page.dart | 114 ++++++++++++++++-- .../subtitle_audio_seek_control_page.dart | 112 +++++++++++++++++ .../subtitle_options_dialog_page.dart | 3 +- .../player/player_bottom_bar_options.dart | 10 ++ yuuna/lib/utils.dart | 1 + 8 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart create mode 100644 yuuna/lib/src/utils/player/player_bottom_bar_options.dart diff --git a/yuuna/lib/i18n/strings.g.dart b/yuuna/lib/i18n/strings.g.dart index 62236158..02405e5c 100644 --- a/yuuna/lib/i18n/strings.g.dart +++ b/yuuna/lib/i18n/strings.g.dart @@ -426,6 +426,7 @@ class _StringsEn implements BaseTranslations { String get local_media_directory_empty => 'Directory has no folders or video'; String get pick_video_file => 'Pick Video File'; String get pin_player_bottom_bar => 'Pin Player Bottom Bar'; + String get seek_control => 'Seek Control'; String get navigate_up_one_directory_level => 'Navigate Up One Directory Level'; String get play => 'Play'; @@ -1106,6 +1107,8 @@ extension on _StringsEn { return 'Pick Video File'; case 'pin_player_bottom_bar': return 'Pin Player Bottom Bar'; + case 'seek_control': + return 'Seek Control'; case 'navigate_up_one_directory_level': return 'Navigate Up One Directory Level'; case 'play': diff --git a/yuuna/lib/pages.dart b/yuuna/lib/pages.dart index 38ae1c03..830c5178 100644 --- a/yuuna/lib/pages.dart +++ b/yuuna/lib/pages.dart @@ -35,6 +35,7 @@ export 'src/pages/implementations/reader_ttu_source_page.dart'; export 'src/pages/implementations/reader_ttu_source_history_page.dart'; export 'src/pages/implementations/reader_lyrics_page.dart'; export 'src/pages/implementations/subtitle_options_dialog_page.dart'; +export 'src/pages/implementations/subtitle_audio_seek_control_page.dart'; export 'src/pages/implementations/placeholder_source_page.dart'; export 'src/pages/implementations/player_transcript_page.dart'; export 'src/pages/implementations/youtube_video_results_page.dart'; diff --git a/yuuna/lib/src/models/app_model.dart b/yuuna/lib/src/models/app_model.dart index b4fcc9a0..63f873c5 100644 --- a/yuuna/lib/src/models/app_model.dart +++ b/yuuna/lib/src/models/app_model.dart @@ -624,7 +624,15 @@ class AppModel with ChangeNotifier { return _currentSubtitleOptions; } + /// Current player bottom bar options. + ValueNotifier? get currentPlayerBottomBarOptions { + _currentPlayerBottomBarOptions ??= + ValueNotifier(playerBottomBarOptions); + return _currentPlayerBottomBarOptions; + } + ValueNotifier? _currentSubtitleOptions; + ValueNotifier? _currentPlayerBottomBarOptions; /// Override color for the dictionary widget. Color? get overrideDictionaryColor => _overrideDictionaryColor; @@ -2446,6 +2454,7 @@ class AppModel with ChangeNotifier { } _currentSubtitleOptions = ValueNotifier(subtitleOptions); + _currentPlayerBottomBarOptions = ValueNotifier(playerBottomBarOptions); _overrideDictionaryColor = null; _overrideDictionaryTheme = null; @@ -3270,11 +3279,26 @@ class AppModel with ChangeNotifier { _preferences.put( 'subtitle_background_opacity', options.subtitleBackgroundOpacity); _preferences.put('subtitle_outline_width', options.subtitleOutlineWidth); + _preferences.put('subtitle_outline_color', options.subtitleOutlineColor); _preferences.put('subtitle_background_blur_radius', options.subtitleBackgroundBlurRadius); _preferences.put('subtitle_above_bar', options.alwaysAboveBottomBar); } + /// Get the bottom bar options used in the player. + PlayerBottomBarOptions get playerBottomBarOptions { + bool keepShown = _preferences.get('keep_shown', defaultValue: false); + + return PlayerBottomBarOptions( + keepShown: keepShown, + ); + } + + /// Set the subtitle options used in the player. + void setPlayerBottomBarOptions(PlayerBottomBarOptions options) { + _preferences.put('keep_shown', options.keepShown); + } + /// Gets the last used audio index of a given media item. int getMediaItemPreferredAudioIndex(MediaItem item) { return _preferences.get('audio_index/${item.uniqueKey}', defaultValue: 0); diff --git a/yuuna/lib/src/pages/implementations/player_source_page.dart b/yuuna/lib/src/pages/implementations/player_source_page.dart index ea4153bb..f4624544 100644 --- a/yuuna/lib/src/pages/implementations/player_source_page.dart +++ b/yuuna/lib/src/pages/implementations/player_source_page.dart @@ -81,6 +81,8 @@ class _PlayerSourcePageState extends BaseSourcePageState late final ValueNotifier _blurOptionsNotifier; late final ValueNotifier _subtitleOptionsNotifier; + late final ValueNotifier + _playerBottomBarOptionsNotifier; StreamSubscription? _playPauseSubscription; StreamSubscription? _seekSubscription; @@ -394,7 +396,10 @@ class _PlayerSourcePageState extends BaseSourcePageState appModel.currentPlayerController = _playerController; _currentSubtitle = appModel.currentSubtitle; _subtitleOptionsNotifier = appModel.currentSubtitleOptions!; - + _playerBottomBarOptionsNotifier = appModel.currentPlayerBottomBarOptions!; + _isMenuShownPermanent.value = + _playerBottomBarOptionsNotifier.value.keepShown; + _isMenuHidden.value = !_playerBottomBarOptionsNotifier.value.keepShown; _currentSubtitle.value = null; appModel.blockCreatorInitialMedia = true; @@ -1226,6 +1231,8 @@ class _PlayerSourcePageState extends BaseSourcePageState children: [ const Space.small(), buildPinButton(), + buildPrevSubtitleSeekButton(), + buildNextSubtitleSeekButton(), buildPlayButton(), buildDurationAndPosition(), buildSlider(), @@ -1263,11 +1270,96 @@ class _PlayerSourcePageState extends BaseSourcePageState } }); } + + PlayerBottomBarOptions playerBottomBarOptions = + appModel.playerBottomBarOptions; + playerBottomBarOptions.keepShown = _isMenuShownPermanent.value; + appModel.setPlayerBottomBarOptions(playerBottomBarOptions); }, ), ); } + /// This shows the Fast Forward button in the bottomleft of the screen. + Widget buildPrevSubtitleSeekButton() { + return MultiValueListenableBuilder( + valueListenables: [ + _durationNotifier, + _positionNotifier, + _endedNotifier, + ], + builder: (context, values, _) { + Duration duration = values.elementAt(0); + Duration position = values.elementAt(1); + bool isEnded = values.elementAt(2); + + bool validPosition = duration.compareTo(position) >= 0; + double sliderValue = validPosition ? position.inSeconds.toDouble() : 0; + + if (isEnded) { + sliderValue = 1; + } + + return Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.fast_rewind, + tooltip: t.seek_control, + onTap: () async { + if (validPosition) { + _playerController.setTime((sliderValue.toInt() - 5) * 1000); + _listeningSubtitle.value = getNearestSubtitle(); + _autoPauseNotifier.value = null; + _autoPauseMemory = null; + } + }, + ), + ); + }, + ); + } + + /// This shows the Fast Forward button in the bottomleft of the screen. + Widget buildNextSubtitleSeekButton() { + return MultiValueListenableBuilder( + valueListenables: [ + _durationNotifier, + _positionNotifier, + _endedNotifier, + ], + builder: (context, values, _) { + Duration duration = values.elementAt(0); + Duration position = values.elementAt(1); + bool isEnded = values.elementAt(2); + + bool validPosition = duration.compareTo(position) >= 0; + double sliderValue = validPosition ? position.inSeconds.toDouble() : 0; + + if (isEnded) { + sliderValue = 1; + } + + return Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.fast_forward, + tooltip: t.seek_control, + onTap: () async { + if (validPosition) { + _playerController.setTime((sliderValue.toInt() + 5) * 1000); + _listeningSubtitle.value = getNearestSubtitle(); + _autoPauseNotifier.value = null; + _autoPauseMemory = null; + } + }, + ), + ); + }, + ); + } + /// This shows the play/pause button in the bottomleft of the screen. Widget buildPlayButton() { return MultiValueListenableBuilder( @@ -2568,15 +2660,17 @@ class _PlayerSourcePageState extends BaseSourcePageState /// This hides or shows the menu. void toggleMenuVisibility() async { - Wakelock.enable(); - _menuHideTimer?.cancel(); - _isMenuHidden.value = !_isMenuHidden.value; - if (!_isMenuHidden.value) { - _menuHideTimer = Timer(const Duration(seconds: 3), () { - if (_playingNotifier.value) { - _isMenuHidden.value = true; - } - }); + if (!_playerBottomBarOptionsNotifier.value.keepShown) { + Wakelock.enable(); + _menuHideTimer?.cancel(); + _isMenuHidden.value = !_isMenuHidden.value; + if (!_isMenuHidden.value) { + _menuHideTimer = Timer(const Duration(seconds: 3), () { + if (_playingNotifier.value) { + _isMenuHidden.value = true; + } + }); + } } } diff --git a/yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart b/yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart new file mode 100644 index 00000000..1f8e9248 --- /dev/null +++ b/yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:spaces/spaces.dart'; +import 'package:yuuna/pages.dart'; +import 'package:yuuna/utils.dart'; + +/// The content of the dialog when editing [SubtitleOptions]. +class SubtitleAudioSeekControlDialogPage extends BasePage { + /// Create an instance of this page. + const SubtitleAudioSeekControlDialogPage({ + required this.notifier, + super.key, + }); + + /// Notifier for the subtitle options. + final ValueNotifier notifier; + + @override + BasePageState createState() => _SubtitleAudioSeekControlDialogPage(); +} + +class _SubtitleAudioSeekControlDialogPage + extends BasePageState { + // late SubtitleOptions _options; + + @override + void initState() { + super.initState(); + // _options = widget.notifier.value; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: MediaQuery.of(context).orientation == Orientation.portrait + ? Spacing.of(context).insets.exceptBottom.big + : Spacing.of(context).insets.exceptBottom.normal.copyWith( + left: Spacing.of(context).spaces.semiBig, + right: Spacing.of(context).spaces.semiBig, + ), + actionsPadding: Spacing.of(context).insets.exceptBottom.normal.copyWith( + left: Spacing.of(context).spaces.normal, + right: Spacing.of(context).spaces.normal, + bottom: Spacing.of(context).spaces.normal, + top: Spacing.of(context).spaces.extraSmall, + ), + content: buildContent(), + actions: actions, + ); + } + + Widget buildContent() { + ScrollController scrollController = ScrollController(); + return RawScrollbar( + thickness: 3, + thumbVisibility: true, + controller: scrollController, + child: Padding( + padding: Spacing.of(context).insets.onlyRight.normal, + child: SingleChildScrollView( + controller: scrollController, + child: SizedBox( + width: MediaQuery.of(context).size.width * (1 / 3), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text('text')], + ), + ), + ), + ), + ); + } + + Future setValues({required bool saveOptions}) async { + Navigator.pop(context); + } + + List get actions => [ + buildSaveButton(), + buildSetButton(), + ]; + + Widget buildSaveButton() { + return TextButton( + onPressed: executeSave, + child: Text( + t.dialog_save, + ), + ); + } + + Widget buildSetButton() { + return TextButton( + onPressed: executeSet, + child: Text( + t.dialog_set, + ), + ); + } + + void executeCancel() async { + Navigator.pop(context); + } + + void executeSave() async { + await setValues(saveOptions: true); + } + + void executeSet() async { + await setValues(saveOptions: false); + } +} diff --git a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart index deade6e0..73a39481 100644 --- a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart @@ -386,7 +386,6 @@ class _SubtitleOptionsDialogPage const Space.small(), TextField( controller: _outlineColorController, - readOnly: true, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: t.player_option_outline_color, @@ -406,7 +405,7 @@ class _SubtitleOptionsDialogPage size: 18, tooltip: t.reset, onTap: () async { - _fontColorController.text = ''; + _outlineColorController.text = ''; FocusScope.of(context).unfocus(); }, icon: Icons.undo, diff --git a/yuuna/lib/src/utils/player/player_bottom_bar_options.dart b/yuuna/lib/src/utils/player/player_bottom_bar_options.dart new file mode 100644 index 00000000..6458b98e --- /dev/null +++ b/yuuna/lib/src/utils/player/player_bottom_bar_options.dart @@ -0,0 +1,10 @@ +/// Settings that are persisted for the bottom bar used in the player. +class PlayerBottomBarOptions { + /// Initialise this object. + PlayerBottomBarOptions({ + required this.keepShown, + }); + + /// Audio allowance, used for audio export, in milliseconds. + bool keepShown; +} diff --git a/yuuna/lib/utils.dart b/yuuna/lib/utils.dart index c061364e..b7755964 100644 --- a/yuuna/lib/utils.dart +++ b/yuuna/lib/utils.dart @@ -25,6 +25,7 @@ export 'src/utils/player/subtitle_utils.dart'; export 'src/utils/player/player_payload.dart'; export 'src/utils/player/blur_options.dart'; export 'src/utils/player/subtitle_options.dart'; +export 'src/utils/player/player_bottom_bar_options.dart'; export 'src/utils/player/youtube_quality.dart'; export 'src/utils/player/playback_mode.dart'; From f72cfa883f0be47b575759fefc786b8305776c1f Mon Sep 17 00:00:00 2001 From: SmartDevStar Date: Thu, 20 Jul 2023 03:42:09 -0600 Subject: [PATCH 4/7] fixed issues for first milestone --- yuuna/lib/src/pages/implementations/player_source_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yuuna/lib/src/pages/implementations/player_source_page.dart b/yuuna/lib/src/pages/implementations/player_source_page.dart index f4624544..0630b0c8 100644 --- a/yuuna/lib/src/pages/implementations/player_source_page.dart +++ b/yuuna/lib/src/pages/implementations/player_source_page.dart @@ -2660,7 +2660,7 @@ class _PlayerSourcePageState extends BaseSourcePageState /// This hides or shows the menu. void toggleMenuVisibility() async { - if (!_playerBottomBarOptionsNotifier.value.keepShown) { + if (!_isMenuShownPermanent.value) { Wakelock.enable(); _menuHideTimer?.cancel(); _isMenuHidden.value = !_isMenuHidden.value; From a76e669c66405d0474dfb6bc8c64c20fd45c09a4 Mon Sep 17 00:00:00 2001 From: SmartDevStar Date: Sat, 22 Jul 2023 01:06:53 -0600 Subject: [PATCH 5/7] completed subtitle font settings for video player --- yuuna/lib/i18n/strings.g.dart | 18 ++ yuuna/lib/pages.dart | 2 +- yuuna/lib/src/models/app_model.dart | 33 +- .../implementations/player_source_page.dart | 298 ++++++++++++------ .../player_transcript_page.dart | 14 +- ...player_volume_brightness_control_page.dart | 212 +++++++++++++ .../subtitle_audio_seek_control_page.dart | 112 ------- .../subtitle_options_dialog_page.dart | 15 +- .../utils/player/player_basic_options.dart | 18 ++ .../player/player_bottom_bar_options.dart | 10 - yuuna/lib/utils.dart | 2 +- 11 files changed, 486 insertions(+), 248 deletions(-) create mode 100644 yuuna/lib/src/pages/implementations/player_volume_brightness_control_page.dart delete mode 100644 yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart create mode 100644 yuuna/lib/src/utils/player/player_basic_options.dart delete mode 100644 yuuna/lib/src/utils/player/player_bottom_bar_options.dart diff --git a/yuuna/lib/i18n/strings.g.dart b/yuuna/lib/i18n/strings.g.dart index 02405e5c..decbf415 100644 --- a/yuuna/lib/i18n/strings.g.dart +++ b/yuuna/lib/i18n/strings.g.dart @@ -436,9 +436,15 @@ class _StringsEn implements BaseTranslations { String get replay => 'Replay'; String get audio_subtitles => 'Audio/Subtitles'; String get player_option_shadowing => 'Shadowing Mode'; + String get player_option_volume_brightness => 'Change Volume & Brightness'; + String get player_option_volume_down => 'Volume Down'; + String get player_option_volume_up => 'Volume Up'; + String get player_option_brightness_down => 'Brightness Down'; + String get player_option_brightness_up => 'Brightness Up'; String get player_option_change_mode => 'Change Playback Mode'; String get player_option_listening_comprehension => 'Listening Comprehension Mode'; + String get player_option_pin_bottom_bar => 'Toggle to Pin Player Bottom Bar'; String get player_option_drag_to_select => 'Use Drag to Select Subtitle Selection'; String get player_option_tap_to_select => @@ -1125,10 +1131,22 @@ extension on _StringsEn { return 'Audio/Subtitles'; case 'player_option_shadowing': return 'Shadowing Mode'; + case 'player_option_volume_brightness': + return 'Change Volume & Brightness'; + case 'player_option_volume_down': + return 'Volume Down'; + case 'player_option_volume_up': + return 'Volume Up'; + case 'player_option_brightness_down': + return 'Brightness Down'; + case 'player_option_brightness_up': + return 'Brightness Up'; case 'player_option_change_mode': return 'Change Playback Mode'; case 'player_option_listening_comprehension': return 'Listening Comprehension Mode'; + case 'player_option_pin_bottom_bar': + return 'Toggle to Pin Player Bottom Bar'; case 'player_option_drag_to_select': return 'Use Drag to Select Subtitle Selection'; case 'player_option_tap_to_select': diff --git a/yuuna/lib/pages.dart b/yuuna/lib/pages.dart index 830c5178..709c8630 100644 --- a/yuuna/lib/pages.dart +++ b/yuuna/lib/pages.dart @@ -35,7 +35,7 @@ export 'src/pages/implementations/reader_ttu_source_page.dart'; export 'src/pages/implementations/reader_ttu_source_history_page.dart'; export 'src/pages/implementations/reader_lyrics_page.dart'; export 'src/pages/implementations/subtitle_options_dialog_page.dart'; -export 'src/pages/implementations/subtitle_audio_seek_control_page.dart'; +export 'src/pages/implementations/player_volume_brightness_control_page.dart'; export 'src/pages/implementations/placeholder_source_page.dart'; export 'src/pages/implementations/player_transcript_page.dart'; export 'src/pages/implementations/youtube_video_results_page.dart'; diff --git a/yuuna/lib/src/models/app_model.dart b/yuuna/lib/src/models/app_model.dart index 63f873c5..29ef58c4 100644 --- a/yuuna/lib/src/models/app_model.dart +++ b/yuuna/lib/src/models/app_model.dart @@ -625,14 +625,14 @@ class AppModel with ChangeNotifier { } /// Current player bottom bar options. - ValueNotifier? get currentPlayerBottomBarOptions { - _currentPlayerBottomBarOptions ??= - ValueNotifier(playerBottomBarOptions); - return _currentPlayerBottomBarOptions; + ValueNotifier? get currentPlayerBasicOptions { + _currentPlayerBasicOptions ??= + ValueNotifier(playerBasicOptions); + return _currentPlayerBasicOptions; } ValueNotifier? _currentSubtitleOptions; - ValueNotifier? _currentPlayerBottomBarOptions; + ValueNotifier? _currentPlayerBasicOptions; /// Override color for the dictionary widget. Color? get overrideDictionaryColor => _overrideDictionaryColor; @@ -2454,7 +2454,7 @@ class AppModel with ChangeNotifier { } _currentSubtitleOptions = ValueNotifier(subtitleOptions); - _currentPlayerBottomBarOptions = ValueNotifier(playerBottomBarOptions); + _currentPlayerBasicOptions = ValueNotifier(playerBasicOptions); _overrideDictionaryColor = null; _overrideDictionaryTheme = null; @@ -3233,18 +3233,18 @@ class AppModel with ChangeNotifier { SubtitleOptions get subtitleOptions { int audioAllowance = _preferences.get('audio_allowance', defaultValue: 0); int subtitleDelay = _preferences.get('subtitle_delay', defaultValue: 0); - double fontSize = _preferences.get('font_size', defaultValue: 20.0); + double fontSize = _preferences.get('font_size', defaultValue: 25.0); String fontName = _preferences .get('font_name/${targetLanguage.languageCode}', defaultValue: ''); - int fontColor = _preferences.get('font_color', defaultValue: 0xff00EE); + int fontColor = _preferences.get('font_color', defaultValue: 0xffffffff); String fontWeight = _preferences.get('font_weight', defaultValue: 'Normal'); String regexFilter = _preferences.get('regex_filter', defaultValue: ''); double subtitleBackgroundOpacity = _preferences.get('subtitle_background_opacity', defaultValue: 0.0); double subtitleOutlineWidth = - _preferences.get('subtitle_outline_width', defaultValue: 3.0); + _preferences.get('subtitle_outline_width', defaultValue: 1.0); int subtitleOutlineColor = - _preferences.get('subtitle_outline_color', defaultValue: 0xffffff); + _preferences.get('subtitle_outline_color', defaultValue: 0xffffffff); double subtitleBackgroundBlurRadius = _preferences.get('subtitle_background_blur_radius', defaultValue: 0.0); bool alwaysAboveBottomBar = @@ -3286,17 +3286,20 @@ class AppModel with ChangeNotifier { } /// Get the bottom bar options used in the player. - PlayerBottomBarOptions get playerBottomBarOptions { + PlayerBasicOptions get playerBasicOptions { bool keepShown = _preferences.get('keep_shown', defaultValue: false); + int volume = _preferences.get('volume', defaultValue: 60); + double brightness = _preferences.get('brightness', defaultValue: 1.0); - return PlayerBottomBarOptions( - keepShown: keepShown, - ); + return PlayerBasicOptions( + keepShown: keepShown, volume: volume, brightness: brightness); } /// Set the subtitle options used in the player. - void setPlayerBottomBarOptions(PlayerBottomBarOptions options) { + void setPlayerBasicOptions(PlayerBasicOptions options) { _preferences.put('keep_shown', options.keepShown); + _preferences.put('volume', options.volume); + _preferences.put('brightness', options.brightness); } /// Gets the last used audio index of a given media item. diff --git a/yuuna/lib/src/pages/implementations/player_source_page.dart b/yuuna/lib/src/pages/implementations/player_source_page.dart index 0630b0c8..c225ac0b 100644 --- a/yuuna/lib/src/pages/implementations/player_source_page.dart +++ b/yuuna/lib/src/pages/implementations/player_source_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_vlc_player/flutter_vlc_player.dart'; import 'package:fluttertoast/fluttertoast.dart'; // import 'package:google_fonts/google_fonts.dart'; import 'package:multi_value_listenable_builder/multi_value_listenable_builder.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:receive_intent/receive_intent.dart'; import 'package:share_plus/share_plus.dart'; import 'package:spaces/spaces.dart'; @@ -81,8 +82,7 @@ class _PlayerSourcePageState extends BaseSourcePageState late final ValueNotifier _blurOptionsNotifier; late final ValueNotifier _subtitleOptionsNotifier; - late final ValueNotifier - _playerBottomBarOptionsNotifier; + late final ValueNotifier _playerBottomBarOptionsNotifier; StreamSubscription? _playPauseSubscription; StreamSubscription? _seekSubscription; @@ -246,7 +246,6 @@ class _PlayerSourcePageState extends BaseSourcePageState if (_playerInitialised) { return; } - await Future.delayed(const Duration(seconds: 1), () {}); appModel.currentMediaPauseStream.listen((event) { @@ -396,10 +395,10 @@ class _PlayerSourcePageState extends BaseSourcePageState appModel.currentPlayerController = _playerController; _currentSubtitle = appModel.currentSubtitle; _subtitleOptionsNotifier = appModel.currentSubtitleOptions!; - _playerBottomBarOptionsNotifier = appModel.currentPlayerBottomBarOptions!; + _playerBottomBarOptionsNotifier = appModel.currentPlayerBasicOptions!; _isMenuShownPermanent.value = _playerBottomBarOptionsNotifier.value.keepShown; - _isMenuHidden.value = !_playerBottomBarOptionsNotifier.value.keepShown; + _isMenuHidden.value = !_isMenuShownPermanent.value; _currentSubtitle.value = null; appModel.blockCreatorInitialMedia = true; @@ -531,6 +530,7 @@ class _PlayerSourcePageState extends BaseSourcePageState setState(() { _playerInitialised = true; }); + loadSavedFont(); _playerController.addListener(listener); @@ -1001,7 +1001,7 @@ class _PlayerSourcePageState extends BaseSourcePageState _transcriptOpenNotifier.value = true; _menuHideTimer?.cancel(); - _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; try { await appModel.temporarilyDisableStatusBarHiding(action: () async { @@ -1130,7 +1130,8 @@ class _PlayerSourcePageState extends BaseSourcePageState if (!_isMenuHidden.value) { _menuHideTimer = Timer(const Duration(seconds: 3), () { if (_playingNotifier.value) { - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; } }); } @@ -1157,7 +1158,8 @@ class _PlayerSourcePageState extends BaseSourcePageState if (!_isMenuHidden.value) { _menuHideTimer = Timer(const Duration(seconds: 3), () { if (_playingNotifier.value) { - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; } }); } @@ -1186,33 +1188,16 @@ class _PlayerSourcePageState extends BaseSourcePageState Widget buildMenuArea() { return Align( alignment: Alignment.topCenter, - child: MultiValueListenableBuilder( - valueListenables: [ - _isMenuHidden, - _isMenuShownPermanent, - ], - builder: (context, values, _) { + child: ValueListenableBuilder( + valueListenable: _isMenuHidden, + builder: (context, value, _) { return AnimatedOpacity( - opacity: values[1] - ? 1.0 - : values[0] - ? 0.0 - : 1.0, + opacity: _isMenuHidden.value ? 0.0 : 1.0, duration: const Duration(milliseconds: 200), child: buildMenuContent(), ); }, ), - // ValueListenableBuilder( - // valueListenable: _isMenuHidden, - // builder: (context, value, _) { - // return AnimatedOpacity( - // opacity: value ? 0.0 : 1.0, - // duration: const Duration(milliseconds: 200), - // child: buildMenuContent(), - // ); - // }, - // ), ); } @@ -1230,13 +1215,12 @@ class _PlayerSourcePageState extends BaseSourcePageState child: Row( children: [ const Space.small(), - buildPinButton(), buildPrevSubtitleSeekButton(), - buildNextSubtitleSeekButton(), buildPlayButton(), + buildNextSubtitleSeekButton(), buildDurationAndPosition(), buildSlider(), - buildSourceButton(), + // buildSourceButton(), buildAudioSubtitlesButton(), buildOptionsButton(), const Space.small(), @@ -1248,39 +1232,7 @@ class _PlayerSourcePageState extends BaseSourcePageState ); } - /// This shows the pin button in the bottomleft of the screen. - Widget buildPinButton() { - return Material( - color: Colors.transparent, - child: JidoujishoIconButton( - size: 24, - icon: _isMenuShownPermanent.value - ? Icons.push_pin - : Icons.push_pin_outlined, - tooltip: t.pin_player_bottom_bar, - onTap: () async { - Wakelock.enable(); - _menuHideTimer?.cancel(); - _isMenuShownPermanent.value = !_isMenuShownPermanent.value; - - if (!_isMenuShownPermanent.value) { - _menuHideTimer = Timer(const Duration(seconds: 3), () { - if (_playingNotifier.value) { - _isMenuHidden.value = true; - } - }); - } - - PlayerBottomBarOptions playerBottomBarOptions = - appModel.playerBottomBarOptions; - playerBottomBarOptions.keepShown = _isMenuShownPermanent.value; - appModel.setPlayerBottomBarOptions(playerBottomBarOptions); - }, - ), - ); - } - - /// This shows the Fast Forward button in the bottomleft of the screen. + /// This shows the Fast Rewind button in the bottomleft of the screen. Widget buildPrevSubtitleSeekButton() { return MultiValueListenableBuilder( valueListenables: [ @@ -1309,9 +1261,14 @@ class _PlayerSourcePageState extends BaseSourcePageState onTap: () async { if (validPosition) { _playerController.setTime((sliderValue.toInt() - 5) * 1000); - _listeningSubtitle.value = getNearestSubtitle(); + // _listeningSubtitle.value = getNearestSubtitle(); _autoPauseNotifier.value = null; _autoPauseMemory = null; + // for (Subtitle subtitle in _subtitleItem.controller.subtitles) { + // if (subtitle > _currentSubtitle.value!) { + // _currentSubtitle.value = subtitle; + // } + // } } }, ), @@ -1569,7 +1526,8 @@ class _PlayerSourcePageState extends BaseSourcePageState if (!_isMenuHidden.value) { _menuHideTimer = Timer(const Duration(seconds: 3), () { if (_playingNotifier.value) { - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; } }); } @@ -2177,6 +2135,8 @@ class _PlayerSourcePageState extends BaseSourcePageState /// This lists the options available when the bottom-right option is tapped. List getOptions() { + MediaSource source = widget.item!.getMediaSource(appModel: appModel); + List options = [ JidoujishoBottomSheetOption( label: t.player_option_change_mode, @@ -2204,6 +2164,127 @@ class _PlayerSourcePageState extends BaseSourcePageState refreshSubtitleWidget(); }, ), + if (source is PlayerLocalMediaSource) + JidoujishoBottomSheetOption( + label: t.pick_video_file, + icon: Icons.perm_media, + action: () async { + PlayerLocalMediaSource localMediaSource = source; + bool shouldResume = !_dialogSmartPaused; + dialogSmartPause(); + + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + await Future.delayed(const Duration(milliseconds: 5), () {}); + if (context.mounted) { + await localMediaSource.pickVideoFile( + appModel: appModel, + context: context, + ref: ref, + pushReplacement: true, + onFileSelected: (path) async { + await _playerController.stop(); + }, + ); + } + + if (mounted) { + await Future.delayed(const Duration(milliseconds: 5), () {}); + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + + if (shouldResume) { + await dialogSmartResume(); + } + } + }, + ), + if (source is PlayerYoutubeSource) + JidoujishoBottomSheetOption( + label: t.comments, + icon: Icons.comment_outlined, + action: () async { + clearDictionaryResult(); + + await dialogSmartPause(); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + await Future.delayed(const Duration(milliseconds: 5), () {}); + + try { + widget.source.setShouldGenerateAudio(value: false); + + await appModel.temporarilyDisableStatusBarHiding( + action: () async { + await Navigator.of(context).push( + PageRouteBuilder( + opaque: false, + pageBuilder: (context, _, __) => + PlayerCommentsPage(videoUrl: widget.item!.uniqueKey), + settings: RouteSettings( + name: (PlayerCommentsPage).toString(), + ), + ), + ); + }); + } finally { + widget.source.setShouldGenerateAudio(value: true); + widget.source.setCurrentSentence( + selection: JidoujishoTextSelection( + text: _currentSubtitle.value?.data ?? '', + ), + ); + } + + await Future.delayed(const Duration(milliseconds: 5), () {}); + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + await dialogSmartResume(); + }, + ), + if (source is PlayerYoutubeSource) + JidoujishoBottomSheetOption( + label: t.change_quality, + icon: Icons.video_settings, + action: () async { + PlayerYoutubeSource youtubeSource = source; + StreamManifest manifest = + youtubeSource.getStreamManifest(widget.item!); + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: true, + builder: (context) => JidoujishoBottomSheet( + options: getQualityOptions( + manifest: manifest, + ), + ), + ); + }, + ), + JidoujishoBottomSheetOption( + label: t.player_option_pin_bottom_bar, + icon: _isMenuShownPermanent.value + ? Icons.push_pin + : Icons.push_pin_outlined, + active: _isMenuShownPermanent.value, + action: () async { + _isMenuShownPermanent.value = !_isMenuShownPermanent.value; + _menuHideTimer?.cancel(); + if (_isMenuShownPermanent.value) { + _isMenuHidden.value = false; + } else { + _menuHideTimer = Timer(const Duration(seconds: 3), () { + if (_playingNotifier.value) { + _isMenuHidden.value = true; + } + }); + } + + PlayerBasicOptions playerBasicOptions = appModel.playerBasicOptions; + playerBasicOptions.keepShown = _isMenuShownPermanent.value; + appModel.setPlayerBasicOptions(playerBasicOptions); + }, + ), JidoujishoBottomSheetOption( label: t.player_change_player_orientation, icon: appModel.isPlayerOrientationPortrait @@ -2291,6 +2372,27 @@ class _PlayerSourcePageState extends BaseSourcePageState } }, ), + JidoujishoBottomSheetOption( + label: t.player_option_volume_brightness, + icon: Icons.volume_up, + action: () async { + bool shouldResume = !_dialogSmartPaused; + await dialogSmartPause(); + if (context.mounted) { + await showDialog( + context: context, + builder: (context) => PlayerVolumeBrightnessControlPage( + notifier: _playerBottomBarOptionsNotifier, + playerController: _playerController, + ), + ); + } + + if (shouldResume) { + await dialogSmartResume(); + } + }, + ), ]; return options; @@ -2493,18 +2595,6 @@ class _PlayerSourcePageState extends BaseSourcePageState : FontWeight.bold, foreground: subtitlePaintStyle, ); - // TextStyle get subtitleOutlineStyle => - // _subtitleOptionsNotifier.value.fontName.trim().isEmpty - // ? TextStyle( - // fontSize: _subtitleOptionsNotifier.value.fontSize, - // foreground: subtitlePaintStyle, - // ) - // : GoogleFonts.getFont( - - // _subtitleOptionsNotifier.value.fontName, - // fontSize: _subtitleOptionsNotifier.value.fontSize, - // foreground: subtitlePaintStyle, - // ); /// Subtitle text style. TextStyle get subtitleTextStyle => TextStyle( @@ -2517,17 +2607,6 @@ class _PlayerSourcePageState extends BaseSourcePageState : FontWeight.bold, color: Color(_subtitleOptionsNotifier.value.fontColor), ); - // TextStyle get subtitleTextStyle => - // _subtitleOptionsNotifier.value.fontName.trim().isEmpty - // ? TextStyle( - // fontSize: _subtitleOptionsNotifier.value.fontSize, - // color: Color(_subtitleOptionsNotifier.value.fontColor), - // ) - // : GoogleFonts.getFont( - // _subtitleOptionsNotifier.value.fontName, - // fontSize: _subtitleOptionsNotifier.value.fontSize, - // color: Color(_subtitleOptionsNotifier.value.fontColor), - // ); /// This is used to set the search term upon pressing on a character /// or selecting text. @@ -2660,7 +2739,9 @@ class _PlayerSourcePageState extends BaseSourcePageState /// This hides or shows the menu. void toggleMenuVisibility() async { - if (!_isMenuShownPermanent.value) { + if (_isMenuShownPermanent.value) { + _isMenuHidden.value = false; + } else { Wakelock.enable(); _menuHideTimer?.cancel(); _isMenuHidden.value = !_isMenuHidden.value; @@ -2697,7 +2778,8 @@ class _PlayerSourcePageState extends BaseSourcePageState _playPauseAnimationController.forward(); _menuHideTimer?.cancel(); - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; }); } else { _playPauseAnimationController.forward(); @@ -2708,7 +2790,8 @@ class _PlayerSourcePageState extends BaseSourcePageState _session.setActive(true); await Future.delayed(const Duration(seconds: 2), () async { _menuHideTimer?.cancel(); - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; await _playerController.seekTo(Duration.zero); _autoPauseNotifier.value = null; @@ -2717,7 +2800,8 @@ class _PlayerSourcePageState extends BaseSourcePageState }); } else { _menuHideTimer?.cancel(); - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; await _playerController.play(); _session.setActive(true); @@ -2796,11 +2880,13 @@ class _PlayerSourcePageState extends BaseSourcePageState if (_dialogSmartPaused) { if (hideInstantly) { _menuHideTimer?.cancel(); - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; } else { _menuHideTimer = Timer(const Duration(seconds: 3), () { if (_playingNotifier.value) { - _isMenuHidden.value = true; + // _isMenuHidden.value = true; + _isMenuHidden.value = !_isMenuShownPermanent.value; } }); } @@ -2856,4 +2942,24 @@ class _PlayerSourcePageState extends BaseSourcePageState widget.source.clearCurrentSentence(); refreshSubtitleWidget(); } + + Future loadSavedFont() async { + try { + Directory appDirectory = await getApplicationDocumentsDirectory(); + String savedFontFilePath = + '${appDirectory.path}/${_subtitleOptionsNotifier.value.fontName}'; + File file = File(savedFontFilePath); + // ignore: avoid_slow_async_io + bool isExisting = file.existsSync(); + if (isExisting) { + FontLoader custom = FontLoader(_subtitleOptionsNotifier.value.fontName); + + Uint8List bytes = await file.readAsBytes(); + custom.addFont(Future.value(ByteData.view(bytes.buffer))); + await custom.load(); + } + } catch (e) { + rethrow; + } + } } diff --git a/yuuna/lib/src/pages/implementations/player_transcript_page.dart b/yuuna/lib/src/pages/implementations/player_transcript_page.dart index c7f36753..51225b5f 100644 --- a/yuuna/lib/src/pages/implementations/player_transcript_page.dart +++ b/yuuna/lib/src/pages/implementations/player_transcript_page.dart @@ -6,7 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_vlc_player/flutter_vlc_player.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:google_fonts/google_fonts.dart'; +// import 'package:google_fonts/google_fonts.dart'; import 'package:material_floating_search_bar/material_floating_search_bar.dart'; import 'package:multi_value_listenable_builder/multi_value_listenable_builder.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -689,14 +689,10 @@ class _PlayerTranscriptPageState FocusScope.of(context).unfocus(); } - TextStyle? get style => widget.subtitleOptions.fontName.trim().isEmpty - ? TextStyle( - fontSize: widget.subtitleOptions.fontSize, - ) - : GoogleFonts.getFont( - widget.subtitleOptions.fontName, - fontSize: widget.subtitleOptions.fontSize, - ); + TextStyle? get style => TextStyle( + fontFamily: widget.subtitleOptions.fontName, + fontSize: widget.subtitleOptions.fontSize, + ); @override void clearDictionaryResult() { diff --git a/yuuna/lib/src/pages/implementations/player_volume_brightness_control_page.dart b/yuuna/lib/src/pages/implementations/player_volume_brightness_control_page.dart new file mode 100644 index 00000000..dc83c3db --- /dev/null +++ b/yuuna/lib/src/pages/implementations/player_volume_brightness_control_page.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:spaces/spaces.dart'; +import 'package:yuuna/pages.dart'; +import 'package:yuuna/utils.dart'; +import 'package:flutter_vlc_player/flutter_vlc_player.dart'; +import 'package:screen_brightness/screen_brightness.dart'; + +/// The content of the dialog when editing [SubtitleOptions]. +class PlayerVolumeBrightnessControlPage extends BasePage { + /// Create an instance of this page. + const PlayerVolumeBrightnessControlPage({ + required this.notifier, + required this.playerController, + super.key, + }); + + /// Notifier for the subtitle options. + final ValueNotifier notifier; + + /// Player controller for the volume control. + final VlcPlayerController playerController; + + @override + BasePageState createState() => _PlayerVolumeBrightnessControlPage(); +} + +class _PlayerVolumeBrightnessControlPage + extends BasePageState { + late PlayerBasicOptions _options; + late VlcPlayerController playerController; + final ValueNotifier _volume = ValueNotifier(60); + final ValueNotifier _brightness = ValueNotifier(1); + + @override + void initState() { + super.initState(); + _options = widget.notifier.value; + _volume.value = _options.volume; + _brightness.value = _options.brightness; + ScreenBrightness().setScreenBrightness(_options.brightness); + playerController = widget.playerController; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: MediaQuery.of(context).orientation == Orientation.portrait + ? Spacing.of(context).insets.exceptBottom.big + : Spacing.of(context).insets.exceptBottom.normal.copyWith( + left: Spacing.of(context).spaces.semiBig, + right: Spacing.of(context).spaces.semiBig, + ), + actionsPadding: Spacing.of(context).insets.exceptBottom.normal.copyWith( + left: Spacing.of(context).spaces.normal, + right: Spacing.of(context).spaces.normal, + bottom: Spacing.of(context).spaces.normal, + top: Spacing.of(context).spaces.extraSmall, + ), + content: buildContent(), + actions: actions, + ); + } + + Widget buildContent() { + ScrollController scrollController = ScrollController(); + return RawScrollbar( + thickness: 3, + thumbVisibility: false, + controller: scrollController, + child: Padding( + padding: Spacing.of(context).insets.onlyRight.normal, + child: SingleChildScrollView( + controller: scrollController, + child: SizedBox( + width: MediaQuery.of(context).size.width * (1 / 3), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.volume_down, + tooltip: t.player_option_volume_down, + ), + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _volume, + builder: (context, value, _) { + return Slider( + activeColor: Colors.red, + max: 100, + inactiveColor: + Theme.of(context).unselectedWidgetColor, + value: value.toDouble(), + onChanged: (value) { + playerController.setVolume(value.toInt()); + _volume.value = value.toInt(); + }, + ); + }, + ), + ), + Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.volume_up, + tooltip: t.player_option_volume_up, + ), + ), + ], + ), + Row( + children: [ + Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.brightness_2, + tooltip: t.player_option_brightness_down, + ), + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _brightness, + builder: (context, value, _) { + return Slider( + activeColor: Colors.red, + inactiveColor: + Theme.of(context).unselectedWidgetColor, + value: value, + onChanged: (value) { + _brightness.value = value; + ScreenBrightness().setScreenBrightness(value); + }, + ); + }, + ), + ), + Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.brightness_7, + tooltip: t.player_option_brightness_up, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Future setValues({required bool saveOptions}) async { + PlayerBasicOptions playerBasicOptions = appModel.playerBasicOptions; + + playerBasicOptions.keepShown = _options.keepShown; + playerBasicOptions.volume = _volume.value; + playerBasicOptions.brightness = _brightness.value; + + widget.notifier.value = playerBasicOptions; + + if (saveOptions) { + appModel.setPlayerBasicOptions(playerBasicOptions); + } + Navigator.pop(context); + } + + List get actions => [ + buildSaveButton(), + buildSetButton(), + ]; + + Widget buildSaveButton() { + return TextButton( + onPressed: executeSave, + child: Text( + t.dialog_save, + ), + ); + } + + Widget buildSetButton() { + return TextButton( + onPressed: executeSet, + child: Text( + t.dialog_set, + ), + ); + } + + void executeCancel() async { + Navigator.pop(context); + } + + void executeSave() async { + await setValues(saveOptions: true); + } + + void executeSet() async { + await setValues(saveOptions: false); + } +} diff --git a/yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart b/yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart deleted file mode 100644 index 1f8e9248..00000000 --- a/yuuna/lib/src/pages/implementations/subtitle_audio_seek_control_page.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:spaces/spaces.dart'; -import 'package:yuuna/pages.dart'; -import 'package:yuuna/utils.dart'; - -/// The content of the dialog when editing [SubtitleOptions]. -class SubtitleAudioSeekControlDialogPage extends BasePage { - /// Create an instance of this page. - const SubtitleAudioSeekControlDialogPage({ - required this.notifier, - super.key, - }); - - /// Notifier for the subtitle options. - final ValueNotifier notifier; - - @override - BasePageState createState() => _SubtitleAudioSeekControlDialogPage(); -} - -class _SubtitleAudioSeekControlDialogPage - extends BasePageState { - // late SubtitleOptions _options; - - @override - void initState() { - super.initState(); - // _options = widget.notifier.value; - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - contentPadding: MediaQuery.of(context).orientation == Orientation.portrait - ? Spacing.of(context).insets.exceptBottom.big - : Spacing.of(context).insets.exceptBottom.normal.copyWith( - left: Spacing.of(context).spaces.semiBig, - right: Spacing.of(context).spaces.semiBig, - ), - actionsPadding: Spacing.of(context).insets.exceptBottom.normal.copyWith( - left: Spacing.of(context).spaces.normal, - right: Spacing.of(context).spaces.normal, - bottom: Spacing.of(context).spaces.normal, - top: Spacing.of(context).spaces.extraSmall, - ), - content: buildContent(), - actions: actions, - ); - } - - Widget buildContent() { - ScrollController scrollController = ScrollController(); - return RawScrollbar( - thickness: 3, - thumbVisibility: true, - controller: scrollController, - child: Padding( - padding: Spacing.of(context).insets.onlyRight.normal, - child: SingleChildScrollView( - controller: scrollController, - child: SizedBox( - width: MediaQuery.of(context).size.width * (1 / 3), - child: const Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text('text')], - ), - ), - ), - ), - ); - } - - Future setValues({required bool saveOptions}) async { - Navigator.pop(context); - } - - List get actions => [ - buildSaveButton(), - buildSetButton(), - ]; - - Widget buildSaveButton() { - return TextButton( - onPressed: executeSave, - child: Text( - t.dialog_save, - ), - ); - } - - Widget buildSetButton() { - return TextButton( - onPressed: executeSet, - child: Text( - t.dialog_set, - ), - ); - } - - void executeCancel() async { - Navigator.pop(context); - } - - void executeSave() async { - await setValues(saveOptions: true); - } - - void executeSet() async { - await setValues(saveOptions: false); - } -} diff --git a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart index 73a39481..1d565ae9 100644 --- a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; // import 'package:google_fonts/google_fonts.dart'; import 'package:spaces/spaces.dart'; // import 'package:url_launcher/url_launcher_string.dart'; @@ -473,11 +474,11 @@ class _SubtitleOptionsDialogPage double? newFontSize = double.tryParse(fontSizeText); String fontColorText = _fontColorController.text; - int? newFontColor = int.tryParse(fontColorText.replaceFirst('#', '0xFF')); + int? newFontColor = int.tryParse(fontColorText.replaceFirst('#', '0x')); String outlineColorText = _outlineColorController.text; int? newOutlineColor = - int.tryParse(outlineColorText.replaceFirst('#', '0xFF')); + int.tryParse(outlineColorText.replaceFirst('#', '0x')); String newFontName = _fontNameController.text.trim(); String newRegexFilter = _regexFilterController.text.trim(); @@ -579,10 +580,16 @@ class _SubtitleOptionsDialogPage allowedExtensions: ['ttf', 'otf'], ); if (result != null) { + File file = File(result.files.single.path ?? ''); + Directory appDirectory = await getApplicationDocumentsDirectory(); + String savedFilePath = + '${appDirectory.path}/${result.files.single.name.split('.').first}'; + File newFile = File(savedFilePath); + await newFile.writeAsBytes(await file.readAsBytes()); _fontNameController.text = result.files.single.name.split('.').first; var custom = FontLoader(_fontNameController.text); - File file = File(result.files.single.path ?? ''); - Uint8List bytes = await file.readAsBytes(); + + Uint8List bytes = await newFile.readAsBytes(); custom.addFont(Future.value(ByteData.view(bytes.buffer))); await custom.load(); return true; diff --git a/yuuna/lib/src/utils/player/player_basic_options.dart b/yuuna/lib/src/utils/player/player_basic_options.dart new file mode 100644 index 00000000..f36ca1a6 --- /dev/null +++ b/yuuna/lib/src/utils/player/player_basic_options.dart @@ -0,0 +1,18 @@ +/// Settings that are persisted for the player. +class PlayerBasicOptions { + /// Initialise this object. + PlayerBasicOptions({ + required this.keepShown, + required this.volume, + required this.brightness, + }); + + /// Keep player bottom bar shown. + bool keepShown; + + /// Player volume. + int volume; + + /// Player brightness + double brightness; +} diff --git a/yuuna/lib/src/utils/player/player_bottom_bar_options.dart b/yuuna/lib/src/utils/player/player_bottom_bar_options.dart deleted file mode 100644 index 6458b98e..00000000 --- a/yuuna/lib/src/utils/player/player_bottom_bar_options.dart +++ /dev/null @@ -1,10 +0,0 @@ -/// Settings that are persisted for the bottom bar used in the player. -class PlayerBottomBarOptions { - /// Initialise this object. - PlayerBottomBarOptions({ - required this.keepShown, - }); - - /// Audio allowance, used for audio export, in milliseconds. - bool keepShown; -} diff --git a/yuuna/lib/utils.dart b/yuuna/lib/utils.dart index b7755964..8e2e8a7e 100644 --- a/yuuna/lib/utils.dart +++ b/yuuna/lib/utils.dart @@ -25,7 +25,7 @@ export 'src/utils/player/subtitle_utils.dart'; export 'src/utils/player/player_payload.dart'; export 'src/utils/player/blur_options.dart'; export 'src/utils/player/subtitle_options.dart'; -export 'src/utils/player/player_bottom_bar_options.dart'; +export 'src/utils/player/player_basic_options.dart'; export 'src/utils/player/youtube_quality.dart'; export 'src/utils/player/playback_mode.dart'; From 2822424b26efca0a2578711650ae50aecf3d134d Mon Sep 17 00:00:00 2001 From: SmartDevStar Date: Sun, 23 Jul 2023 10:06:59 +0300 Subject: [PATCH 6/7] added subtitle seek and fullscreen mode functionality --- yuuna/lib/i18n/strings.g.dart | 18 +- yuuna/lib/pages.dart | 1 + yuuna/lib/src/models/app_model.dart | 8 +- .../implementations/player_source_page.dart | 262 ++++++++++-------- .../subtitle_options_dialog_page.dart | 8 +- .../subtitle_seek_dialog_page.dart | 209 ++++++++++++++ .../utils/player/player_basic_options.dart | 4 + 7 files changed, 391 insertions(+), 119 deletions(-) create mode 100644 yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart diff --git a/yuuna/lib/i18n/strings.g.dart b/yuuna/lib/i18n/strings.g.dart index decbf415..1bc7c044 100644 --- a/yuuna/lib/i18n/strings.g.dart +++ b/yuuna/lib/i18n/strings.g.dart @@ -431,6 +431,8 @@ class _StringsEn implements BaseTranslations { 'Navigate Up One Directory Level'; String get play => 'Play'; String get pause => 'Pause'; + String get prev_subtitle => 'Prev Subtitle'; + String get next_subtitle => 'Next Subtitle'; String get record => 'Record'; String get stop => 'Stop'; String get replay => 'Replay'; @@ -445,6 +447,7 @@ class _StringsEn implements BaseTranslations { String get player_option_listening_comprehension => 'Listening Comprehension Mode'; String get player_option_pin_bottom_bar => 'Toggle to Pin Player Bottom Bar'; + String get player_option_fullscreen_mode => 'Toggle to Set FullScreen Mode'; String get player_option_drag_to_select => 'Use Drag to Select Subtitle Selection'; String get player_option_tap_to_select => @@ -470,6 +473,7 @@ class _StringsEn implements BaseTranslations { 'Align Subtitle with Transcript'; String get player_option_subtitle_appearance => 'Subtitle Timing and Appearance'; + String get player_option_head_to_target_subtitle => 'Head to Target Subtitle'; String get player_option_load_subtitles => 'Load External Subtitles'; String get player_option_subtitle_delay => 'Subtitle delay'; String get player_option_audio_allowance => 'Audio allowance'; @@ -623,8 +627,6 @@ class _StringsEn implements BaseTranslations { String get api_key => 'API Key'; String subtitle_delay_set({required Object ms}) => 'Subtitle delay set to ${ms} ms.'; - String get cancel => 'Cancel'; - String get choose_color => 'Choose'; String get server_port_in_use => 'Local server port already in use'; String get google_fonts => 'Google Fonts'; String get pick_color => 'Pick Color'; @@ -1121,6 +1123,10 @@ extension on _StringsEn { return 'Play'; case 'pause': return 'Pause'; + case 'prev_subtitle': + return 'Prev Subtitle'; + case 'next_subtitle': + return 'Next Subtitle'; case 'record': return 'Record'; case 'stop': @@ -1147,6 +1153,8 @@ extension on _StringsEn { return 'Listening Comprehension Mode'; case 'player_option_pin_bottom_bar': return 'Toggle to Pin Player Bottom Bar'; + case 'player_option_fullscreen_mode': + return 'Toggle to Set FullScreen Mode'; case 'player_option_drag_to_select': return 'Use Drag to Select Subtitle Selection'; case 'player_option_tap_to_select': @@ -1187,6 +1195,8 @@ extension on _StringsEn { return 'Align Subtitle with Transcript'; case 'player_option_subtitle_appearance': return 'Subtitle Timing and Appearance'; + case 'player_option_head_to_target_subtitle': + return 'Head to Target Subtitle'; case 'player_option_load_subtitles': return 'Load External Subtitles'; case 'player_option_subtitle_delay': @@ -1450,10 +1460,6 @@ extension on _StringsEn { return 'API Key'; case 'subtitle_delay_set': return ({required Object ms}) => 'Subtitle delay set to ${ms} ms.'; - case 'cancel': - return 'Cancel'; - case 'choose_color': - return 'Choose'; case 'server_port_in_use': return 'Local server port already in use'; case 'google_fonts': diff --git a/yuuna/lib/pages.dart b/yuuna/lib/pages.dart index 709c8630..dd4bb3bf 100644 --- a/yuuna/lib/pages.dart +++ b/yuuna/lib/pages.dart @@ -36,6 +36,7 @@ export 'src/pages/implementations/reader_ttu_source_history_page.dart'; export 'src/pages/implementations/reader_lyrics_page.dart'; export 'src/pages/implementations/subtitle_options_dialog_page.dart'; export 'src/pages/implementations/player_volume_brightness_control_page.dart'; +export 'src/pages/implementations/subtitle_seek_dialog_page.dart'; export 'src/pages/implementations/placeholder_source_page.dart'; export 'src/pages/implementations/player_transcript_page.dart'; export 'src/pages/implementations/youtube_video_results_page.dart'; diff --git a/yuuna/lib/src/models/app_model.dart b/yuuna/lib/src/models/app_model.dart index 29ef58c4..3e893438 100644 --- a/yuuna/lib/src/models/app_model.dart +++ b/yuuna/lib/src/models/app_model.dart @@ -3288,11 +3288,16 @@ class AppModel with ChangeNotifier { /// Get the bottom bar options used in the player. PlayerBasicOptions get playerBasicOptions { bool keepShown = _preferences.get('keep_shown', defaultValue: false); + bool keepSysNavbarShown = + _preferences.get('keep_system_navbar_shown', defaultValue: false); int volume = _preferences.get('volume', defaultValue: 60); double brightness = _preferences.get('brightness', defaultValue: 1.0); return PlayerBasicOptions( - keepShown: keepShown, volume: volume, brightness: brightness); + keepShown: keepShown, + volume: volume, + brightness: brightness, + keepSysNavbarShown: keepSysNavbarShown); } /// Set the subtitle options used in the player. @@ -3300,6 +3305,7 @@ class AppModel with ChangeNotifier { _preferences.put('keep_shown', options.keepShown); _preferences.put('volume', options.volume); _preferences.put('brightness', options.brightness); + _preferences.put('keep_system_navbar_shown', options.keepSysNavbarShown); } /// Gets the last used audio index of a given media item. diff --git a/yuuna/lib/src/pages/implementations/player_source_page.dart b/yuuna/lib/src/pages/implementations/player_source_page.dart index c225ac0b..8d7b1449 100644 --- a/yuuna/lib/src/pages/implementations/player_source_page.dart +++ b/yuuna/lib/src/pages/implementations/player_source_page.dart @@ -82,7 +82,7 @@ class _PlayerSourcePageState extends BaseSourcePageState late final ValueNotifier _blurOptionsNotifier; late final ValueNotifier _subtitleOptionsNotifier; - late final ValueNotifier _playerBottomBarOptionsNotifier; + late final ValueNotifier _playerBasicOptionsNotifier; StreamSubscription? _playPauseSubscription; StreamSubscription? _seekSubscription; @@ -395,9 +395,8 @@ class _PlayerSourcePageState extends BaseSourcePageState appModel.currentPlayerController = _playerController; _currentSubtitle = appModel.currentSubtitle; _subtitleOptionsNotifier = appModel.currentSubtitleOptions!; - _playerBottomBarOptionsNotifier = appModel.currentPlayerBasicOptions!; - _isMenuShownPermanent.value = - _playerBottomBarOptionsNotifier.value.keepShown; + _playerBasicOptionsNotifier = appModel.currentPlayerBasicOptions!; + _isMenuShownPermanent.value = _playerBasicOptionsNotifier.value.keepShown; _isMenuHidden.value = !_isMenuShownPermanent.value; _currentSubtitle.value = null; appModel.blockCreatorInitialMedia = true; @@ -531,6 +530,7 @@ class _PlayerSourcePageState extends BaseSourcePageState _playerInitialised = true; }); loadSavedFont(); + setScreenMode(); _playerController.addListener(listener); @@ -941,7 +941,10 @@ class _PlayerSourcePageState extends BaseSourcePageState return GestureDetector( child: buildScrubDetectors(), onTap: () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } + toggleMenuVisibility(); }, onHorizontalDragStart: (details) { @@ -994,8 +997,9 @@ class _PlayerSourcePageState extends BaseSourcePageState if (!appModel.isTranscriptPlayerMode) { await dialogSmartPause(); } - - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } await Future.delayed(const Duration(milliseconds: 5), () {}); _transcriptOpenNotifier.value = true; @@ -1025,8 +1029,10 @@ class _PlayerSourcePageState extends BaseSourcePageState onTap: (index) async { final navigator = Navigator.of(context); await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode( - SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + } navigator.pop(); await _playerController.seekTo( _subtitleItem.controller.subtitles[index].start - @@ -1074,7 +1080,6 @@ class _PlayerSourcePageState extends BaseSourcePageState } await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); } } @@ -1220,7 +1225,6 @@ class _PlayerSourcePageState extends BaseSourcePageState buildNextSubtitleSeekButton(), buildDurationAndPosition(), buildSlider(), - // buildSourceButton(), buildAudioSubtitlesButton(), buildOptionsButton(), const Space.small(), @@ -1234,86 +1238,59 @@ class _PlayerSourcePageState extends BaseSourcePageState /// This shows the Fast Rewind button in the bottomleft of the screen. Widget buildPrevSubtitleSeekButton() { - return MultiValueListenableBuilder( - valueListenables: [ - _durationNotifier, - _positionNotifier, - _endedNotifier, - ], - builder: (context, values, _) { - Duration duration = values.elementAt(0); - Duration position = values.elementAt(1); - bool isEnded = values.elementAt(2); - - bool validPosition = duration.compareTo(position) >= 0; - double sliderValue = validPosition ? position.inSeconds.toDouble() : 0; - - if (isEnded) { - sliderValue = 1; - } - - return Material( - color: Colors.transparent, - child: JidoujishoIconButton( - size: 24, - icon: Icons.fast_rewind, - tooltip: t.seek_control, - onTap: () async { - if (validPosition) { - _playerController.setTime((sliderValue.toInt() - 5) * 1000); - // _listeningSubtitle.value = getNearestSubtitle(); - _autoPauseNotifier.value = null; - _autoPauseMemory = null; - // for (Subtitle subtitle in _subtitleItem.controller.subtitles) { - // if (subtitle > _currentSubtitle.value!) { - // _currentSubtitle.value = subtitle; - // } - // } - } - }, - ), - ); - }, + return Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.fast_rewind, + tooltip: t.seek_control, + onTap: () async { + int prevIdx = _subtitleItem.controller.subtitles.lastIndexWhere( + (element) => + _positionNotifier.value > (element.start + subtitleDelay)); + if (prevIdx != -1) { + _playerController.seekTo( + _subtitleItem.controller.subtitles[prevIdx].start - + subtitleDelay); + _autoPauseNotifier.value = null; + _autoPauseMemory = null; + _bufferingNotifier.value = true; + _listeningSubtitle.value = getNearestSubtitle(); + refreshSubtitleWidget(); + // _currentSubtitle.value = + // _subtitleItem.controller.subtitles[prevIdx]; + } + }, + ), ); } /// This shows the Fast Forward button in the bottomleft of the screen. Widget buildNextSubtitleSeekButton() { - return MultiValueListenableBuilder( - valueListenables: [ - _durationNotifier, - _positionNotifier, - _endedNotifier, - ], - builder: (context, values, _) { - Duration duration = values.elementAt(0); - Duration position = values.elementAt(1); - bool isEnded = values.elementAt(2); - - bool validPosition = duration.compareTo(position) >= 0; - double sliderValue = validPosition ? position.inSeconds.toDouble() : 0; - - if (isEnded) { - sliderValue = 1; - } - - return Material( - color: Colors.transparent, - child: JidoujishoIconButton( - size: 24, - icon: Icons.fast_forward, - tooltip: t.seek_control, - onTap: () async { - if (validPosition) { - _playerController.setTime((sliderValue.toInt() + 5) * 1000); - _listeningSubtitle.value = getNearestSubtitle(); - _autoPauseNotifier.value = null; - _autoPauseMemory = null; - } - }, - ), - ); - }, + return Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.fast_forward, + tooltip: t.seek_control, + onTap: () async { + int nextIdx = _subtitleItem.controller.subtitles.indexWhere( + (element) => + _positionNotifier.value < (element.start - subtitleDelay)); + if (nextIdx != -1) { + _playerController.seekTo( + _subtitleItem.controller.subtitles[nextIdx].start - + subtitleDelay); + _autoPauseNotifier.value = null; + _autoPauseMemory = null; + _bufferingNotifier.value = true; + _listeningSubtitle.value = getNearestSubtitle(); + refreshSubtitleWidget(); + // _currentSubtitle.value = + // _subtitleItem.controller.subtitles[nextIdx]; + } + }, + ), ); } @@ -1580,8 +1557,9 @@ class _PlayerSourcePageState extends BaseSourcePageState onTap: () async { bool shouldResume = !_dialogSmartPaused; dialogSmartPause(); - - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } await Future.delayed(const Duration(milliseconds: 5), () {}); if (context.mounted) { await source.pickVideoFile( @@ -1597,8 +1575,10 @@ class _PlayerSourcePageState extends BaseSourcePageState if (mounted) { await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode( - SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + } if (shouldResume) { await dialogSmartResume(); @@ -1648,7 +1628,9 @@ class _PlayerSourcePageState extends BaseSourcePageState clearDictionaryResult(); await dialogSmartPause(); - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } await Future.delayed(const Duration(milliseconds: 5), () {}); try { @@ -1676,8 +1658,10 @@ class _PlayerSourcePageState extends BaseSourcePageState } await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode( - SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + } await dialogSmartResume(); }, ), @@ -1796,8 +1780,9 @@ class _PlayerSourcePageState extends BaseSourcePageState bool shouldResume = !_dialogSmartPaused; await dialogSmartPause(); - - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } await Future.delayed(const Duration(milliseconds: 5), () {}); _transcriptOpenNotifier.value = true; @@ -1853,14 +1838,40 @@ class _PlayerSourcePageState extends BaseSourcePageState _transcriptOpenNotifier.value = false; await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode( - SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + } if (shouldResume) { await dialogSmartResume(); } }, ), + // JidoujishoBottomSheetOption( + // label: t.player_option_head_to_target_subtitle, + // icon: Icons.swipe_left, + // action: () async { + // bool shouldResume = !_dialogSmartPaused; + // await dialogSmartPause(); + // if (context.mounted) { + // await showDialog( + // context: context, + // builder: (context) => SubtitleSeekDialogPage( + // playerController: _playerController, + // subtitleOptionsNotifier: _subtitleOptionsNotifier, + // subtitleItem: _subtitleItem, + // positionNotifier: _positionNotifier, + // currentSubtitle: _currentSubtitle, + // ), + // ); + // } + + // if (shouldResume) { + // await dialogSmartResume(); + // } + // }, + // ), JidoujishoBottomSheetOption( label: t.player_option_subtitle_appearance, icon: Icons.text_fields, @@ -2172,8 +2183,10 @@ class _PlayerSourcePageState extends BaseSourcePageState PlayerLocalMediaSource localMediaSource = source; bool shouldResume = !_dialogSmartPaused; dialogSmartPause(); - - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge); + } await Future.delayed(const Duration(milliseconds: 5), () {}); if (context.mounted) { await localMediaSource.pickVideoFile( @@ -2189,8 +2202,10 @@ class _PlayerSourcePageState extends BaseSourcePageState if (mounted) { await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode( - SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + } if (shouldResume) { await dialogSmartResume(); @@ -2206,7 +2221,10 @@ class _PlayerSourcePageState extends BaseSourcePageState clearDictionaryResult(); await dialogSmartPause(); - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge); + } await Future.delayed(const Duration(milliseconds: 5), () {}); try { @@ -2235,8 +2253,10 @@ class _PlayerSourcePageState extends BaseSourcePageState } await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode( - SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + } await dialogSmartResume(); }, ), @@ -2285,6 +2305,19 @@ class _PlayerSourcePageState extends BaseSourcePageState appModel.setPlayerBasicOptions(playerBasicOptions); }, ), + JidoujishoBottomSheetOption( + label: t.player_option_fullscreen_mode, + icon: _playerBasicOptionsNotifier.value.keepSysNavbarShown + ? Icons.bolt + : Icons.bolt_outlined, + active: _playerBasicOptionsNotifier.value.keepSysNavbarShown, + action: () async { + _playerBasicOptionsNotifier.value.keepSysNavbarShown = + !_playerBasicOptionsNotifier.value.keepSysNavbarShown; + setScreenMode(); + appModel.setPlayerBasicOptions(_playerBasicOptionsNotifier.value); + }, + ), JidoujishoBottomSheetOption( label: t.player_change_player_orientation, icon: appModel.isPlayerOrientationPortrait @@ -2382,7 +2415,7 @@ class _PlayerSourcePageState extends BaseSourcePageState await showDialog( context: context, builder: (context) => PlayerVolumeBrightnessControlPage( - notifier: _playerBottomBarOptionsNotifier, + notifier: _playerBasicOptionsNotifier, playerController: _playerController, ), ); @@ -2676,8 +2709,9 @@ class _PlayerSourcePageState extends BaseSourcePageState } String sentence = buffer.toString().trim(); - - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } await Future.delayed(const Duration(milliseconds: 5), () {}); await appModel.openCreator( @@ -2696,7 +2730,9 @@ class _PlayerSourcePageState extends BaseSourcePageState ); await Future.delayed(const Duration(milliseconds: 5), () {}); - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + if (!_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } } /// This makes the subtitle widget force to reflect a change, for example @@ -2949,7 +2985,6 @@ class _PlayerSourcePageState extends BaseSourcePageState String savedFontFilePath = '${appDirectory.path}/${_subtitleOptionsNotifier.value.fontName}'; File file = File(savedFontFilePath); - // ignore: avoid_slow_async_io bool isExisting = file.existsSync(); if (isExisting) { FontLoader custom = FontLoader(_subtitleOptionsNotifier.value.fontName); @@ -2962,4 +2997,13 @@ class _PlayerSourcePageState extends BaseSourcePageState rethrow; } } + + Future setScreenMode() async { + if (_playerBasicOptionsNotifier.value.keepSysNavbarShown) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + } + } } diff --git a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart index 1d565ae9..68e549ca 100644 --- a/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/subtitle_options_dialog_page.dart @@ -607,7 +607,9 @@ class _SubtitleOptionsDialogPage return AlertDialog( content: SingleChildScrollView( child: ColorPicker( - pickerColor: const Color(0xff443a49), + pickerColor: target == 'Font' + ? Color(_options.fontColor) + : Color(_options.subtitleOutlineColor), paletteType: PaletteType.hueWheel, onColorChanged: (value) { newColor = value; @@ -616,7 +618,7 @@ class _SubtitleOptionsDialogPage ), actions: [ TextButton( - child: Text(t.choose_color), + child: Text(t.dialog_save), onPressed: () { if (target == 'Font') { _fontColorController.text = @@ -629,7 +631,7 @@ class _SubtitleOptionsDialogPage }, ), TextButton( - child: Text(t.cancel), + child: Text(t.dialog_cancel), onPressed: () { Navigator.of(context).pop(); }, diff --git a/yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart b/yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart new file mode 100644 index 00000000..27cba0d3 --- /dev/null +++ b/yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:spaces/spaces.dart'; +import 'package:yuuna/pages.dart'; +import 'package:yuuna/utils.dart'; +import 'package:subtitle/subtitle.dart'; +import 'package:flutter_vlc_player/flutter_vlc_player.dart'; + +/// The content of the dialog when editing [SubtitleOptions]. +class SubtitleSeekDialogPage extends BasePage { + /// Create an instance of this page. + const SubtitleSeekDialogPage({ + required this.playerController, + required this.subtitleOptionsNotifier, + required this.subtitleItem, + required this.positionNotifier, + required this.currentSubtitle, + super.key, + }); + + /// Player controller for the volume control. + final VlcPlayerController playerController; + + /// Notifier for the subtitle options. + final ValueNotifier subtitleOptionsNotifier; + + /// Subtitle Item for current Video + final SubtitleItem subtitleItem; + + /// Notifier for the current position of Video progress. + final ValueNotifier positionNotifier; + + /// Notifier for current subtitle of Video. + final ValueNotifier currentSubtitle; + + @override + BasePageState createState() => _PlayerVolumeBrightnessControlPage(); +} + +class _PlayerVolumeBrightnessControlPage + extends BasePageState { + late VlcPlayerController _playerController; + late SubtitleOptions _subtitleOptions; + late SubtitleItem _subtitleItem; + late ValueNotifier _positionNotifier; + late ValueNotifier _currentSubtitle; + + @override + void initState() { + super.initState(); + _playerController = widget.playerController; + _subtitleOptions = widget.subtitleOptionsNotifier.value; + _subtitleItem = widget.subtitleItem; + _positionNotifier = widget.positionNotifier; + _currentSubtitle = widget.currentSubtitle; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: MediaQuery.of(context).orientation == Orientation.portrait + ? Spacing.of(context).insets.exceptBottom.big + : Spacing.of(context).insets.exceptBottom.normal.copyWith( + left: Spacing.of(context).spaces.semiBig, + right: Spacing.of(context).spaces.semiBig, + ), + actionsPadding: Spacing.of(context).insets.exceptBottom.normal.copyWith( + left: Spacing.of(context).spaces.normal, + right: Spacing.of(context).spaces.normal, + bottom: Spacing.of(context).spaces.normal, + top: Spacing.of(context).spaces.extraSmall, + ), + content: buildContent(), + // actions: actions, + ); + } + + Widget buildContent() { + ScrollController scrollController = ScrollController(); + return RawScrollbar( + thickness: 3, + thumbVisibility: false, + controller: scrollController, + child: Padding( + padding: Spacing.of(context).insets.onlyRight.normal, + child: SingleChildScrollView( + controller: scrollController, + child: SizedBox( + width: MediaQuery.of(context).size.width * (2 / 3), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ValueListenableBuilder( + valueListenable: _currentSubtitle, + builder: (context, value, _) { + return Padding( + padding: Spacing.of(context).insets.all.normal, + child: Text( + value?.data ?? '', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: _subtitleOptions.fontName, + fontSize: 20, + ), + ), + ); + }, + ), + const Space.normal(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.swipe_left, + tooltip: t.prev_subtitle, + onTap: () async { + int prevIdx = _subtitleItem.controller.subtitles + .lastIndexWhere((element) => + _positionNotifier.value > + (element.start + + Duration( + milliseconds: + _subtitleOptions.subtitleDelay))); + if (prevIdx != -1) { + _playerController.seekTo(_subtitleItem + .controller.subtitles[prevIdx].start - + Duration( + milliseconds: + _subtitleOptions.subtitleDelay)); + } + }, + ), + ), + Material( + color: Colors.transparent, + child: JidoujishoIconButton( + size: 24, + icon: Icons.swipe_right, + tooltip: t.next_subtitle, + onTap: () async { + int nextIdx = _subtitleItem.controller.subtitles + .indexWhere((element) => + _positionNotifier.value < + (element.start - + Duration( + milliseconds: + _subtitleOptions.subtitleDelay))); + if (nextIdx != -1) { + _playerController.seekTo(_subtitleItem + .controller.subtitles[nextIdx].start - + Duration( + milliseconds: + _subtitleOptions.subtitleDelay)); + } + }, + ), + ), + ], + ), + const SizedBox(height: 15), + ], + ), + ), + ), + ), + ); + } + + Future setValues({required bool saveOptions}) async { + Navigator.pop(context); + } + + List get actions => [ + buildSaveButton(), + buildSetButton(), + ]; + + Widget buildSaveButton() { + return TextButton( + onPressed: executeSave, + child: Text( + t.dialog_save, + ), + ); + } + + Widget buildSetButton() { + return TextButton( + onPressed: executeSet, + child: Text( + t.dialog_set, + ), + ); + } + + void executeCancel() async { + Navigator.pop(context); + } + + void executeSave() async { + await setValues(saveOptions: true); + } + + void executeSet() async { + await setValues(saveOptions: false); + } +} diff --git a/yuuna/lib/src/utils/player/player_basic_options.dart b/yuuna/lib/src/utils/player/player_basic_options.dart index f36ca1a6..3f31a7dc 100644 --- a/yuuna/lib/src/utils/player/player_basic_options.dart +++ b/yuuna/lib/src/utils/player/player_basic_options.dart @@ -5,6 +5,7 @@ class PlayerBasicOptions { required this.keepShown, required this.volume, required this.brightness, + required this.keepSysNavbarShown, }); /// Keep player bottom bar shown. @@ -15,4 +16,7 @@ class PlayerBasicOptions { /// Player brightness double brightness; + + /// Keep system navigation bar shown. + bool keepSysNavbarShown; } From 6ff8296fd09df879b88cdaa54ef1ce2379057fe3 Mon Sep 17 00:00:00 2001 From: SmartDevStar Date: Mon, 24 Jul 2023 13:08:42 -0600 Subject: [PATCH 7/7] completed subtitle settings of video player --- .../implementations/player_source_page.dart | 164 +++++++++++------- .../subtitle_seek_dialog_page.dart | 82 +++++---- 2 files changed, 152 insertions(+), 94 deletions(-) diff --git a/yuuna/lib/src/pages/implementations/player_source_page.dart b/yuuna/lib/src/pages/implementations/player_source_page.dart index 8d7b1449..2b89bb6d 100644 --- a/yuuna/lib/src/pages/implementations/player_source_page.dart +++ b/yuuna/lib/src/pages/implementations/player_source_page.dart @@ -818,12 +818,29 @@ class _PlayerSourcePageState extends BaseSourcePageState @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: onWillPop, - child: Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: Colors.black, - body: buildBody(), + return RawKeyboardListener( + focusNode: FocusNode(), + autofocus: true, + onKey: (event) async { + if (event.runtimeType == RawKeyDownEvent) { + if (event.isKeyPressed(LogicalKeyboardKey.keyP)) { + await playPause(); + } + if (event.isKeyPressed(LogicalKeyboardKey.keyB)) { + seekPrevSubtitle(); + } + if (event.isKeyPressed(LogicalKeyboardKey.keyF)) { + seekNextSubtitle(); + } + } + }, + child: WillPopScope( + onWillPop: onWillPop, + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.black, + body: buildBody(), + ), ), ); } @@ -1245,21 +1262,7 @@ class _PlayerSourcePageState extends BaseSourcePageState icon: Icons.fast_rewind, tooltip: t.seek_control, onTap: () async { - int prevIdx = _subtitleItem.controller.subtitles.lastIndexWhere( - (element) => - _positionNotifier.value > (element.start + subtitleDelay)); - if (prevIdx != -1) { - _playerController.seekTo( - _subtitleItem.controller.subtitles[prevIdx].start - - subtitleDelay); - _autoPauseNotifier.value = null; - _autoPauseMemory = null; - _bufferingNotifier.value = true; - _listeningSubtitle.value = getNearestSubtitle(); - refreshSubtitleWidget(); - // _currentSubtitle.value = - // _subtitleItem.controller.subtitles[prevIdx]; - } + seekPrevSubtitle(); }, ), ); @@ -1274,21 +1277,7 @@ class _PlayerSourcePageState extends BaseSourcePageState icon: Icons.fast_forward, tooltip: t.seek_control, onTap: () async { - int nextIdx = _subtitleItem.controller.subtitles.indexWhere( - (element) => - _positionNotifier.value < (element.start - subtitleDelay)); - if (nextIdx != -1) { - _playerController.seekTo( - _subtitleItem.controller.subtitles[nextIdx].start - - subtitleDelay); - _autoPauseNotifier.value = null; - _autoPauseMemory = null; - _bufferingNotifier.value = true; - _listeningSubtitle.value = getNearestSubtitle(); - refreshSubtitleWidget(); - // _currentSubtitle.value = - // _subtitleItem.controller.subtitles[nextIdx]; - } + seekNextSubtitle(); }, ), ); @@ -1848,30 +1837,32 @@ class _PlayerSourcePageState extends BaseSourcePageState } }, ), - // JidoujishoBottomSheetOption( - // label: t.player_option_head_to_target_subtitle, - // icon: Icons.swipe_left, - // action: () async { - // bool shouldResume = !_dialogSmartPaused; - // await dialogSmartPause(); - // if (context.mounted) { - // await showDialog( - // context: context, - // builder: (context) => SubtitleSeekDialogPage( - // playerController: _playerController, - // subtitleOptionsNotifier: _subtitleOptionsNotifier, - // subtitleItem: _subtitleItem, - // positionNotifier: _positionNotifier, - // currentSubtitle: _currentSubtitle, - // ), - // ); - // } - - // if (shouldResume) { - // await dialogSmartResume(); - // } - // }, - // ), + JidoujishoBottomSheetOption( + label: t.seek_control, + icon: Icons.swipe_left, + action: () async { + bool shouldResume = !_dialogSmartPaused; + await dialogSmartPause(); + if (context.mounted) { + await showDialog( + context: context, + builder: (context) => SubtitleSeekDialogPage( + playerController: _playerController, + subtitleOptionsNotifier: _subtitleOptionsNotifier, + positionNotifier: _positionNotifier, + currentSubtitle: _currentSubtitle, + durationNotifier: _durationNotifier, + endedNotifier: _endedNotifier, + autoPauseNotifier: _autoPauseNotifier, + ), + ); + } + + if (shouldResume) { + await dialogSmartResume(); + } + }, + ), JidoujishoBottomSheetOption( label: t.player_option_subtitle_appearance, icon: Icons.text_fields, @@ -3006,4 +2997,57 @@ class _PlayerSourcePageState extends BaseSourcePageState SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); } } + + void seekPrevSubtitle() { + Duration duration = _durationNotifier.value; + Duration position = _positionNotifier.value; + bool isEnded = _endedNotifier.value; + + bool validPosition = duration.compareTo(position) >= 0; + double sliderValue = validPosition ? position.inSeconds.toDouble() : 0; + + if (isEnded) { + sliderValue = 1; + } + int prevIdx = _subtitleItem.controller.subtitles.lastIndexWhere( + (element) => _positionNotifier.value > (element.start + subtitleDelay)); + if (validPosition && prevIdx != -1) { + // _playerController.setTime( + // _subtitleItem.controller.subtitles[prevIdx].start.inSeconds - + // subtitleDelay.inSeconds); + int deltaTime = _positionNotifier.value.inSeconds - + _subtitleItem.controller.subtitles[prevIdx].start.inSeconds + + subtitleDelay.inSeconds; + _playerController.setTime((sliderValue.toInt() - deltaTime) * 1000); + _autoPauseNotifier.value = null; + _autoPauseMemory = null; + } + } + + void seekNextSubtitle() { + Duration duration = _durationNotifier.value; + Duration position = _positionNotifier.value; + bool isEnded = _endedNotifier.value; + + bool validPosition = duration.compareTo(position) >= 0; + double sliderValue = validPosition ? position.inSeconds.toDouble() : 0; + + if (isEnded) { + sliderValue = 1; + } + int nextIdx = _subtitleItem.controller.subtitles.indexWhere( + (element) => _positionNotifier.value < (element.start - subtitleDelay)); + if (validPosition && nextIdx != -1) { + int deltaTime = + _subtitleItem.controller.subtitles[nextIdx].start.inSeconds - + _positionNotifier.value.inSeconds - + subtitleDelay.inSeconds; + _playerController.setTime((sliderValue.toInt() + deltaTime) * 1000); + // _playerController.setTime( + // _subtitleItem.controller.subtitles[nextIdx].start.inSeconds - + // subtitleDelay.inSeconds); + _autoPauseNotifier.value = null; + _autoPauseMemory = null; + } + } } diff --git a/yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart b/yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart index 27cba0d3..0759315c 100644 --- a/yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/subtitle_seek_dialog_page.dart @@ -11,9 +11,11 @@ class SubtitleSeekDialogPage extends BasePage { const SubtitleSeekDialogPage({ required this.playerController, required this.subtitleOptionsNotifier, - required this.subtitleItem, required this.positionNotifier, + required this.durationNotifier, + required this.endedNotifier, required this.currentSubtitle, + required this.autoPauseNotifier, super.key, }); @@ -23,15 +25,21 @@ class SubtitleSeekDialogPage extends BasePage { /// Notifier for the subtitle options. final ValueNotifier subtitleOptionsNotifier; - /// Subtitle Item for current Video - final SubtitleItem subtitleItem; - /// Notifier for the current position of Video progress. final ValueNotifier positionNotifier; + /// Notifier for the duration of Video progress. + final ValueNotifier durationNotifier; + + /// Notifier to check the video is finished. + final ValueNotifier endedNotifier; + /// Notifier for current subtitle of Video. final ValueNotifier currentSubtitle; + /// Notifier for auto pause of Video. + final ValueNotifier autoPauseNotifier; + @override BasePageState createState() => _PlayerVolumeBrightnessControlPage(); } @@ -40,18 +48,22 @@ class _PlayerVolumeBrightnessControlPage extends BasePageState { late VlcPlayerController _playerController; late SubtitleOptions _subtitleOptions; - late SubtitleItem _subtitleItem; late ValueNotifier _positionNotifier; + late ValueNotifier _durationNotifier; + late ValueNotifier _endedNotifier; late ValueNotifier _currentSubtitle; + late ValueNotifier _autoPauseNotifier; @override void initState() { super.initState(); _playerController = widget.playerController; _subtitleOptions = widget.subtitleOptionsNotifier.value; - _subtitleItem = widget.subtitleItem; _positionNotifier = widget.positionNotifier; + _durationNotifier = widget.durationNotifier; + _endedNotifier = widget.endedNotifier; _currentSubtitle = widget.currentSubtitle; + _autoPauseNotifier = widget.autoPauseNotifier; } @override @@ -114,21 +126,22 @@ class _PlayerVolumeBrightnessControlPage child: JidoujishoIconButton( size: 24, icon: Icons.swipe_left, - tooltip: t.prev_subtitle, + tooltip: t.seek_control, onTap: () async { - int prevIdx = _subtitleItem.controller.subtitles - .lastIndexWhere((element) => - _positionNotifier.value > - (element.start + - Duration( - milliseconds: - _subtitleOptions.subtitleDelay))); - if (prevIdx != -1) { - _playerController.seekTo(_subtitleItem - .controller.subtitles[prevIdx].start - - Duration( - milliseconds: - _subtitleOptions.subtitleDelay)); + Duration duration = _durationNotifier.value; + Duration position = _positionNotifier.value; + bool isEnded = _endedNotifier.value; + bool validPosition = + duration.compareTo(position) >= 0; + double sliderValue = + validPosition ? position.inSeconds.toDouble() : 0; + if (isEnded) { + sliderValue = 1; + } + if (validPosition) { + _playerController + .setTime((sliderValue.toInt() - 5) * 1000); + _autoPauseNotifier.value = null; } }, ), @@ -138,21 +151,22 @@ class _PlayerVolumeBrightnessControlPage child: JidoujishoIconButton( size: 24, icon: Icons.swipe_right, - tooltip: t.next_subtitle, + tooltip: t.seek_control, onTap: () async { - int nextIdx = _subtitleItem.controller.subtitles - .indexWhere((element) => - _positionNotifier.value < - (element.start - - Duration( - milliseconds: - _subtitleOptions.subtitleDelay))); - if (nextIdx != -1) { - _playerController.seekTo(_subtitleItem - .controller.subtitles[nextIdx].start - - Duration( - milliseconds: - _subtitleOptions.subtitleDelay)); + Duration duration = _durationNotifier.value; + Duration position = _positionNotifier.value; + bool isEnded = _endedNotifier.value; + bool validPosition = + duration.compareTo(position) >= 0; + double sliderValue = + validPosition ? position.inSeconds.toDouble() : 0; + if (isEnded) { + sliderValue = 1; + } + if (validPosition) { + _playerController + .setTime((sliderValue.toInt() + 5) * 1000); + _autoPauseNotifier.value = null; } }, ),