i18n(fr): extract hardcoded strings, harmonize tutoiement, audit cohérence

- Extract 13 hardcoded user-facing strings from cloud, collection, player,
  debrid, IntroDB to res/values/strings.xml (FR translations included)
- Refactor titleForMedia() to accept localized suffixes as parameters,
  wrap addTmdbSourceFromInput/addTraktSourceFromResult in scope.launch
- Pre-load error message for runCatching error() to keep i18n-aware
- Harmonize entire values-fr/strings.xml to tutoiement (100 occurrences
  converted from vouvoiement: Cliquez→Clique, Vous→Tu, Votre→Ton, etc.)
- Fix terminology inconsistencies: Résumé→Aperçu, Réseaux→Chaînes (TMDB),
  Suivant→À suivre (up next), Téléchargement échoué→Échec du téléchargement,
  Découverte TMDB→TMDB Discover, etc.
- Add 134 EN strings, +444 FR (134 new + 310 backfill/rewrites)
- Parity preserved: 1532 ↔ 1532 strings, 0 placeholder divergence
This commit is contained in:
Stéphane 2026-05-24 09:39:29 +02:00
parent c8c3095517
commit f0736813c4
12 changed files with 981 additions and 761 deletions

View file

@ -1456,145 +1456,165 @@
<string name="unit_bytes_kb">KB</string>
<string name="unit_bytes_mb">MB</string>
<string name="unit_bytes_gb">GB</string>
<string name="plugins_section_overview">OVERVIEW</string>
<string name="plugins_badge_repos">%1$d repos</string>
<string name="plugins_badge_providers">%1$d providers</string>
<string name="plugins_badge_enabled">Plugins enabled</string>
<string name="plugins_badge_disabled">Plugins disabled</string>
<string name="plugins_badge_tmdb_key_set">TMDB API key set</string>
<string name="plugins_badge_tmdb_key_missing">TMDB API key missing</string>
<string name="plugins_tmdb_required_message">Plugin providers require a TMDB API key. Set it on the TMDB screen or plugin providers will not work correctly.</string>
<string name="plugins_enable_globally_title">Enable plugin providers globally</string>
<string name="plugins_enable_globally_desc">Use plugin providers during stream discovery.</string>
<string name="plugins_group_by_repo_title">Group plugin providers by repository</string>
<string name="plugins_group_by_repo_desc">In Streams, show one provider per repository instead of one per source.</string>
<string name="plugins_section_add_repo">ADD REPOSITORY</string>
<string name="plugins_input_manifest_placeholder">Plugin manifest URL</string>
<string name="plugins_button_installing">Installing…</string>
<string name="plugins_button_install_repo">Install Plugin Repository</string>
<string name="plugins_error_enter_repo_url">Enter a plugin repository URL.</string>
<string name="plugins_error_enter_valid_url">Enter a valid plugin URL.</string>
<string name="plugins_error_already_installed">That plugin repository is already installed.</string>
<string name="plugins_error_install_failed">Unable to install plugin repository</string>
<string name="plugins_error_refresh_failed">Unable to refresh repository</string>
<string name="plugins_error_provider_not_found">Provider not found</string>
<string name="plugins_error_unavailable_build">Plugins are not available in this build.</string>
<string name="plugins_manifest_error_name_missing">Manifest name is missing.</string>
<string name="plugins_manifest_error_version_missing">Manifest version is missing.</string>
<string name="plugins_manifest_error_no_providers">Manifest has no providers.</string>
<string name="plugins_message_installed">Installed %1$s.</string>
<string name="plugins_section_installed_repos">INSTALLED REPOSITORIES</string>
<string name="plugins_empty_repos_title">No plugin repositories installed yet.</string>
<string name="plugins_empty_repos_subtitle">Add a repository URL to install provider plugins for stream discovery.</string>
<string name="plugins_repo_version">Version %1$s</string>
<string name="plugins_cd_refresh_repo">Refresh plugin repository</string>
<string name="plugins_cd_delete_repo">Delete plugin repository</string>
<string name="plugins_badge_refreshing">Refreshing</string>
<string name="plugins_section_providers">PROVIDERS</string>
<string name="plugins_empty_providers">No providers available yet.</string>
<string name="plugins_provider_no_description">No description</string>
<string name="plugins_provider_version">v%1$s</string>
<string name="plugins_provider_disabled_by_repo">Disabled by repo</string>
<string name="plugins_button_testing">Testing…</string>
<string name="plugins_button_test_provider">Test Provider</string>
<string name="plugins_test_error_title">Error</string>
<string name="plugins_test_failed">Provider test failed</string>
<string name="plugins_test_results_count">Test results (%1$d)</string>
<string name="plugins_repo_fallback_label">Plugin repository</string>
<string name="submit_intro_action">Submit Intro</string>
<!-- PlayerControls a11y -->
<string name="player_action_submit_intro">Submit Intro</string>
<string name="player_action_video_settings">Video settings</string>
<!-- CloudLibrary -->
<string name="cloud_library_provider_unavailable">Cloud library is not available for %1$s.</string>
<!-- Player iOS video settings modal -->
<string name="player_video_settings_title">Video</string>
<string name="player_video_settings_reset_tuning">Reset tuning</string>
<string name="player_video_settings_output_preset">Output preset</string>
<string name="player_video_settings_hdr_peak_detection">HDR peak detection</string>
<string name="player_video_settings_hdr_peak_detection_desc">Estimate HDR peak brightness when metadata is bad or missing.</string>
<string name="player_video_settings_tone_mapping">Tone mapping</string>
<string name="player_video_settings_deband">Deband</string>
<string name="player_video_settings_deband_desc">Reduce color banding at a small performance cost.</string>
<string name="player_video_settings_interpolation">Frame interpolation</string>
<string name="player_video_settings_interpolation_desc">Smooth motion when mpv can use display sync cleanly.</string>
<string name="player_video_settings_brightness">Brightness</string>
<string name="player_video_settings_contrast">Contrast</string>
<string name="player_video_settings_saturation">Saturation</string>
<string name="player_video_settings_gamma">Gamma</string>
<!-- iOS video output presets -->
<string name="player_ios_preset_native_edr_label">Native EDR</string>
<string name="player_ios_preset_native_edr_desc">Best for HDR-capable iPhones and iPads.</string>
<string name="player_ios_preset_sdr_tone_mapped_label">SDR tone mapped</string>
<string name="player_ios_preset_sdr_tone_mapped_desc">More predictable whites and blacks on SDR-style output.</string>
<string name="player_ios_preset_compatibility_label">Compatibility</string>
<string name="player_ios_preset_compatibility_desc">Closest to the older iOS MPV behavior.</string>
<string name="player_ios_preset_custom_label">Custom</string>
<string name="player_ios_preset_custom_desc">Use your advanced values below.</string>
<string name="player_ios_hardware_decoder_off">Off</string>
<!-- Playback settings iOS video output -->
<string name="settings_playback_ios_video_output">iOS video output</string>
<string name="settings_playback_ios_hardware_decoder">Hardware decoder</string>
<string name="settings_playback_ios_extended_dynamic_range">Extended dynamic range</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Default Metal output mode for new playback sessions.</string>
<string name="settings_playback_ios_display_color_hint">Display color hint</string>
<string name="settings_playback_ios_display_color_hint_desc">Let mpv target the active display color space by default.</string>
<string name="settings_playback_ios_target_primaries">Target primaries</string>
<string name="settings_playback_ios_target_transfer">Target transfer</string>
<!-- Debrid Result Management section -->
<string name="settings_debrid_section_result_management">Result Management</string>
<string name="settings_debrid_max_results">Max results</string>
<string name="settings_debrid_max_results_desc">Limit how many results appear.</string>
<string name="settings_debrid_sort_results">Sort results</string>
<string name="settings_debrid_sort_results_desc">Choose how results are ordered.</string>
<string name="settings_debrid_per_resolution_limit">Per resolution limit</string>
<string name="settings_debrid_per_resolution_limit_desc">Cap repeated 2160p, 1080p, 720p results after sorting.</string>
<string name="settings_debrid_per_quality_limit">Per quality limit</string>
<string name="settings_debrid_per_quality_limit_desc">Cap repeated BluRay, WEB-DL, REMUX results after sorting.</string>
<string name="settings_debrid_size_range">Size range</string>
<string name="settings_debrid_size_range_desc">Filter results by file size.</string>
<!-- Debrid misc -->
<string name="settings_debrid_learn_more">Learn more</string>
<string name="settings_debrid_template_default_format">Default format</string>
<string name="settings_debrid_release_groups_hint">Enter one group per line.</string>
<!-- Debrid sort profiles -->
<string name="settings_debrid_sort_default">Default</string>
<string name="settings_debrid_sort_largest">Largest first</string>
<string name="settings_debrid_sort_smallest">Smallest first</string>
<string name="settings_debrid_sort_best_audio">Best audio first</string>
<string name="settings_debrid_sort_language">Language first</string>
<!-- Debrid selection labels -->
<string name="settings_debrid_selection_any">Any</string>
<string name="settings_debrid_selection_count">%1$d selected</string>
<!-- Debrid result count labels -->
<string name="settings_debrid_results_all">All results</string>
<string name="settings_debrid_results_count">%1$d results</string>
<!-- Debrid size range labels -->
<string name="settings_debrid_size_up_to">Up to %1$dGB</string>
<string name="settings_debrid_size_min">%1$dGB+</string>
<string name="settings_debrid_size_range_value">%1$d-%2$dGB</string>
<!-- Debrid rule rows: resolutions -->
<string name="settings_debrid_rule_preferred_resolutions">Preferred resolutions</string>
<string name="settings_debrid_rule_preferred_resolutions_desc">Sort selected resolutions first, in default order.</string>
<string name="settings_debrid_rule_required_resolutions">Required resolutions</string>
<string name="settings_debrid_rule_required_resolutions_desc">Only show selected resolutions.</string>
<string name="settings_debrid_rule_excluded_resolutions">Excluded resolutions</string>
<string name="settings_debrid_rule_excluded_resolutions_desc">Hide selected resolutions.</string>
<!-- Debrid rule rows: qualities -->
<string name="settings_debrid_rule_preferred_qualities">Preferred qualities</string>
<string name="settings_debrid_rule_preferred_qualities_desc">Sort selected qualities first, in default order.</string>
<string name="settings_debrid_rule_required_qualities">Required qualities</string>
<string name="settings_debrid_rule_required_qualities_desc">Only show selected qualities.</string>
<string name="settings_debrid_rule_excluded_qualities">Excluded qualities</string>
<string name="settings_debrid_rule_excluded_qualities_desc">Hide selected qualities.</string>
<!-- Debrid rule rows: visual tags -->
<string name="settings_debrid_rule_preferred_visual_tags">Preferred visual tags</string>
<string name="settings_debrid_rule_preferred_visual_tags_desc">Sort DV, HDR, 10bit, IMAX and similar tags.</string>
<string name="settings_debrid_rule_required_visual_tags">Required visual tags</string>
<string name="settings_debrid_rule_required_visual_tags_desc">Require DV, HDR, 10bit, IMAX, SDR and similar tags.</string>
<string name="settings_debrid_rule_excluded_visual_tags">Excluded visual tags</string>
<string name="settings_debrid_rule_excluded_visual_tags_desc">Hide DV, HDR, 10bit, 3D and similar tags.</string>
<!-- Debrid rule rows: audio tags -->
<string name="settings_debrid_rule_preferred_audio_tags">Preferred audio tags</string>
<string name="settings_debrid_rule_preferred_audio_tags_desc">Sort Atmos, TrueHD, DTS, AAC and similar tags.</string>
<string name="settings_debrid_rule_required_audio_tags">Required audio tags</string>
<string name="settings_debrid_rule_required_audio_tags_desc">Require Atmos, TrueHD, DTS, AAC and similar tags.</string>
<string name="settings_debrid_rule_excluded_audio_tags">Excluded audio tags</string>
<string name="settings_debrid_rule_excluded_audio_tags_desc">Hide selected audio tags.</string>
<!-- Debrid rule rows: channels -->
<string name="settings_debrid_rule_preferred_channels">Preferred channels</string>
<string name="settings_debrid_rule_preferred_channels_desc">Sort preferred channel layouts first.</string>
<string name="settings_debrid_rule_required_channels">Required channels</string>
<string name="settings_debrid_rule_required_channels_desc">Only show selected channel layouts.</string>
<string name="settings_debrid_rule_excluded_channels">Excluded channels</string>
<string name="settings_debrid_rule_excluded_channels_desc">Hide selected channel layouts.</string>
<!-- Debrid rule rows: encodes -->
<string name="settings_debrid_rule_preferred_encodes">Preferred encodes</string>
<string name="settings_debrid_rule_preferred_encodes_desc">Sort AV1, HEVC, AVC and similar encodes.</string>
<string name="settings_debrid_rule_required_encodes">Required encodes</string>
<string name="settings_debrid_rule_required_encodes_desc">Require AV1, HEVC, AVC and similar encodes.</string>
<string name="settings_debrid_rule_excluded_encodes">Excluded encodes</string>
<string name="settings_debrid_rule_excluded_encodes_desc">Hide selected encodes.</string>
<!-- Debrid rule rows: languages -->
<string name="settings_debrid_rule_preferred_languages">Preferred languages</string>
<string name="settings_debrid_rule_preferred_languages_desc">Sort preferred audio languages first.</string>
<string name="settings_debrid_rule_required_languages">Required languages</string>
<string name="settings_debrid_rule_required_languages_desc">Only show results with selected languages.</string>
<string name="settings_debrid_rule_excluded_languages">Excluded languages</string>
<string name="settings_debrid_rule_excluded_languages_desc">Hide results where every language is excluded.</string>
<!-- Debrid rule rows: release groups -->
<string name="settings_debrid_rule_required_release_groups">Required release groups</string>
<string name="settings_debrid_rule_required_release_groups_desc">Only show selected release groups.</string>
<string name="settings_debrid_rule_excluded_release_groups">Excluded release groups</string>
<string name="settings_debrid_rule_excluded_release_groups_desc">Hide selected release groups.</string>
<!-- Submit intro/recap/outro dialog -->
<string name="submit_intro_title">Submit Timestamps</string>
<string name="submit_intro_segment_type_label">SEGMENT TYPE</string>
<string name="submit_intro_segment_type">SEGMENT TYPE</string>
<string name="submit_intro_segment_intro">Intro</string>
<string name="submit_intro_segment_recap">Recap</string>
<string name="submit_intro_segment_outro">Outro</string>
<string name="submit_intro_start_time_label">START TIME (MM:SS)</string>
<string name="submit_intro_end_time_label">END TIME (MM:SS)</string>
<string name="submit_intro_button_submit">Submit</string>
<string name="submit_intro_capture_button">Capture</string>
<string name="settings_playback_introdb_invalid_key">Invalid API Key or connection failed</string>
<string name="network_connection_issue">Connection issue</string>
<string name="network_please_check_connection">Please check your connection and try again.</string>
<string name="library_local_tab_title">Nuvio Library</string>
<string name="streams_plugin_repository_fallback">Plugin repository</string>
<string name="unit_bytes_tb">TB</string>
<string name="collections_editor_tmdb_genres_movie_placeholder">28,12</string>
<string name="collections_editor_tmdb_genres_series_placeholder">18,35</string>
<string name="collections_editor_tmdb_date_from_placeholder">2020-01-01</string>
<string name="collections_editor_tmdb_date_to_placeholder">2024-12-31</string>
<string name="collections_editor_tmdb_rating_min_placeholder">7.0</string>
<string name="collections_editor_tmdb_rating_max_placeholder">10</string>
<string name="collections_editor_tmdb_votes_min_placeholder">100</string>
<string name="collections_editor_tmdb_language_placeholder">en, ko, ja, hi</string>
<string name="collections_editor_tmdb_country_placeholder">US, KR, JP, IN</string>
<string name="collections_editor_tmdb_year_placeholder">2024</string>
<string name="collections_editor_trakt_list_id_format">ID %1$s</string>
<string name="player_addon_subtitle_display_format">%1$s (%2$s)</string>
<string name="trakt_connected_status">Connected to Trakt</string>
<string name="trakt_disconnected_status">Disconnected from Trakt</string>
<string name="collections_editor_trakt_enter_name_url_or_id">Enter a Trakt list name, URL, or ID</string>
<string name="collections_editor_trakt_enter_id_or_url">Enter a Trakt list ID or URL</string>
<string name="collections_editor_trakt_load_failed">Could not load Trakt list</string>
<string name="collections_editor_trakt_fallback_title">Trakt List %1$d</string>
<string name="collections_editor_trakt_resolved_subtitle">Resolved Trakt list</string>
<string name="submit_intro_start_time">START TIME (MM:SS)</string>
<string name="submit_intro_end_time">END TIME (MM:SS)</string>
<string name="submit_intro_submit">Submit</string>
<string name="submit_intro_capture">Capture</string>
<!-- iOS advanced playback -->
<string name="settings_playback_ios_hw_decoder_dialog">Hardware decoder</string>
<string name="settings_playback_ios_target_primaries_dialog">Target primaries</string>
<string name="settings_playback_ios_target_transfer_dialog">Target transfer</string>
<!-- Collection editor -->
<string name="collections_editor_resolved_trakt_list">Resolved Trakt list</string>
<string name="collections_editor_tmdb_discover">TMDB Discover</string>
<string name="collections_editor_watch_region_placeholder">US, KR, JP, IN</string>
<!-- Collection editor error messages -->
<string name="collections_editor_trakt_input_required">Enter a Trakt list name, URL, or ID</string>
<string name="collections_editor_trakt_no_lists_found">No Trakt lists found</string>
<string name="collections_editor_tmdb_invalid_id_or_url">Enter a valid TMDB ID or URL.</string>
<string name="collections_editor_tmdb_list_title_format">TMDB List %1$s</string>
<string name="collections_editor_tmdb_collection_title_format">TMDB Collection %1$s</string>
<string name="collections_editor_tmdb_production_title_format">TMDB Production %1$s</string>
<string name="collections_editor_tmdb_network_title_format">TMDB Network %1$s</string>
<string name="collections_editor_tmdb_person_title_format">TMDB Person %1$s</string>
<string name="collections_editor_tmdb_director_title_format">TMDB Director %1$s</string>
<string name="collections_editor_tmdb_discover_title">TMDB Discover</string>
<string name="collections_editor_tmdb_source_load_failed">Could not load TMDB source</string>
<string name="collections_folder_trakt_series_list">Trakt Series List</string>
<string name="collections_folder_trakt_movie_list">Trakt Movie List</string>
<string name="collections_tmdb_api_key_required">Add a TMDB API key in Settings to use TMDB sources.</string>
<string name="collections_tmdb_list_not_found">TMDB list not found</string>
<string name="collections_tmdb_collection_not_found">TMDB collection not found</string>
<string name="collections_tmdb_company_not_found">TMDB company not found</string>
<string name="collections_tmdb_network_not_found">TMDB network not found</string>
<string name="collections_tmdb_person_not_found">TMDB person not found</string>
<string name="collections_tmdb_missing_list_id">Missing TMDB list ID</string>
<string name="collections_tmdb_missing_collection_id">Missing TMDB collection ID</string>
<string name="collections_tmdb_missing_person_id">Missing TMDB person ID</string>
<string name="collections_tmdb_person_credits_not_found">TMDB person credits not found</string>
<string name="collections_tmdb_discover_no_data">TMDB discover returned no data</string>
<string name="collections_trakt_missing_list_id">Missing Trakt list ID</string>
<string name="collections_trakt_invalid_list_id_or_url">Enter a valid Trakt list ID or URL</string>
<string name="collections_trakt_missing_numeric_id">Trakt list did not include a numeric ID</string>
<string name="collections_trakt_request_failed">Trakt request failed</string>
<string name="collections_trakt_credentials_missing">Missing Trakt credentials.</string>
<string name="collections_trakt_list_items_count">%1$d items</string>
<string name="collections_trakt_list_likes_count">%1$d likes</string>
<string name="collections_trakt_public_list">Trakt public list</string>
<string name="collections_trakt_list_not_found_or_private">Trakt list not found or not public</string>
<string name="collections_trakt_rate_limit_reached">Trakt rate limit reached</string>
<string name="collections_trakt_error_with_code">%1$s (%2$d)</string>
<string name="details_comments_trakt_load_failed_with_code">Failed to load Trakt comments (%1$d)</string>
<string name="updates_no_channel_release">No update has been published yet.</string>
<string name="updates_github_api_error">GitHub releases API error: %1$d</string>
<string name="updates_release_missing_title">Release has no tag or name</string>
<string name="updates_apk_asset_missing">No APK asset found in the release</string>
<string name="updates_download_failed_http">Download failed with HTTP %1$d</string>
<string name="updates_empty_download_body">Empty download body</string>
<string name="updates_downloaded_file_missing">Downloaded update file is missing.</string>
<string name="network_request_failed_http">Request failed with HTTP %1$d</string>
<string name="network_empty_response_body">Empty response body</string>
<string name="downloads_error_finalize_file_failed">Failed to finalize download file</string>
<string name="downloads_error_open_partial_file_failed">Failed to open partial download file</string>
<string name="downloads_error_partial_file_not_open">Partial download file is not open</string>
<string name="downloads_error_write_partial_file_failed">Failed to write partial download file</string>
<string name="details_runtime_hours_minutes">%1$dh %2$dm</string>
<string name="details_runtime_hours_only">%1$dh</string>
<string name="details_runtime_minutes_only">%1$dm</string>
<string name="search_error_no_results_for_catalog">No search results returned for %1$s.</string>
<string name="plugins_repository_already_installed">That plugin repository is already installed.</string>
<string name="plugins_repository_install_failed">Unable to install plugin repository</string>
<string name="plugins_repository_refresh_failed">Unable to refresh repository</string>
<string name="plugins_manifest_name_missing">Manifest name is missing.</string>
<string name="plugins_manifest_version_missing">Manifest version is missing.</string>
<string name="plugins_manifest_no_providers">Manifest has no providers.</string>
<string name="addons_manifest_missing_field">Manifest missing \"%1$s\"</string>
<string name="player_error_mpv_unavailable">MPV player engine not available. Please rebuild the app.</string>
<string name="player_error_unable_to_play_stream">Unable to play this stream.</string>
<string name="collections_editor_tmdb_invalid_id_error">Enter a valid TMDB ID or URL.</string>
<string name="collections_editor_tmdb_load_error">Could not load TMDB source</string>
<string name="collections_editor_trakt_id_url_required">Enter a Trakt list ID or URL</string>
<string name="collections_editor_trakt_load_error">Could not load Trakt list</string>
<!-- Collection editor TMDB country chips -->
<string name="collections_editor_tmdb_country_ca">Canada</string>
<string name="collections_editor_tmdb_country_au">Australia</string>
<string name="collections_editor_tmdb_country_de">Germany</string>
<!-- Collection editor media type suffixes (appended to source titles) -->
<string name="collections_editor_media_movies_suffix">Movies</string>
<string name="collections_editor_media_series_suffix">Series</string>
<!-- Cloud library -->
<string name="cloud_library_playback_disabled">Cloud library is disabled.</string>
<!-- IntroDB settings -->
<string name="settings_playback_introdb_invalid_api_key">Invalid API Key or connection failed</string>
</resources>

View file

@ -12,6 +12,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.cloud_library_playback_disabled
import nuvio.composeapp.generated.resources.cloud_library_provider_unavailable
import org.jetbrains.compose.resources.getString
internal class CloudLibraryStore(
private val credentialsProvider: suspend () -> List<DebridServiceCredential>,
@ -26,7 +30,10 @@ internal class CloudLibraryStore(
if (api == null) {
return@map CloudLibraryProviderState(
provider = credential.provider,
errorMessage = "Cloud library is not available for ${credential.provider.displayName}.",
errorMessage = getString(
Res.string.cloud_library_provider_unavailable,
credential.provider.displayName,
),
)
}
@ -191,7 +198,7 @@ object CloudLibraryRepository {
): CloudLibraryPlaybackResult {
DebridSettingsRepository.ensureLoaded()
if (!DebridSettingsRepository.snapshot().cloudLibraryEnabled) {
return CloudLibraryPlaybackResult.Failed("Cloud library is disabled.")
return CloudLibraryPlaybackResult.Failed(getString(Res.string.cloud_library_playback_disabled))
}
val result = store.resolvePlayback(item, file)
if (result is CloudLibraryPlaybackResult.Success) {

View file

@ -11,25 +11,17 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.collections_editor_tmdb_collection_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_director_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_discover_title
import nuvio.composeapp.generated.resources.collections_editor_tmdb_invalid_id_or_url
import nuvio.composeapp.generated.resources.collections_editor_tmdb_list_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_network_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_person_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_production_title_format
import nuvio.composeapp.generated.resources.collections_editor_tmdb_source_load_failed
import nuvio.composeapp.generated.resources.collections_editor_trakt_enter_id_or_url
import nuvio.composeapp.generated.resources.collections_editor_trakt_enter_name_url_or_id
import nuvio.composeapp.generated.resources.collections_editor_trakt_fallback_title
import nuvio.composeapp.generated.resources.collections_editor_trakt_load_failed
import nuvio.composeapp.generated.resources.collections_editor_media_movies_suffix
import nuvio.composeapp.generated.resources.collections_editor_media_series_suffix
import nuvio.composeapp.generated.resources.collections_editor_resolved_trakt_list
import nuvio.composeapp.generated.resources.collections_editor_tmdb_discover
import nuvio.composeapp.generated.resources.collections_editor_tmdb_invalid_id_error
import nuvio.composeapp.generated.resources.collections_editor_tmdb_load_error
import nuvio.composeapp.generated.resources.collections_editor_trakt_id_url_required
import nuvio.composeapp.generated.resources.collections_editor_trakt_input_required
import nuvio.composeapp.generated.resources.collections_editor_trakt_load_error
import nuvio.composeapp.generated.resources.collections_editor_trakt_no_lists_found
import nuvio.composeapp.generated.resources.collections_editor_trakt_resolved_subtitle
import nuvio.composeapp.generated.resources.media_movies
import nuvio.composeapp.generated.resources.media_series
import org.jetbrains.compose.resources.getString
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@ -414,23 +406,25 @@ object CollectionEditorRepository {
val state = _uiState.value
val query = state.traktInput.trim()
if (query.isBlank()) {
_uiState.value = state.copy(
traktSearchError = runBlocking { getString(Res.string.collections_editor_trakt_enter_name_url_or_id) },
)
scope.launch {
_uiState.value = _uiState.value.copy(
traktSearchError = getString(Res.string.collections_editor_trakt_input_required),
)
}
return
}
scope.launch {
val loadErrorMessage = getString(Res.string.collections_editor_trakt_load_error)
val results = if (query.isTraktListIdentifierInput()) {
runCatching {
val metadata = TraktPublicListSourceResolver.listImportMetadata(query)
val id = metadata.traktListId
?: error(getString(Res.string.collections_editor_trakt_load_failed))
val id = metadata.traktListId ?: error(loadErrorMessage)
listOf(
TraktPublicListSearchResult(
traktListId = id,
title = metadata.title ?: getString(Res.string.collections_editor_trakt_fallback_title, id),
subtitle = getString(Res.string.collections_editor_trakt_resolved_subtitle),
title = metadata.title ?: "Trakt List $id",
subtitle = getString(Res.string.collections_editor_resolved_trakt_list),
coverImageUrl = metadata.coverImageUrl,
),
)
@ -647,45 +641,46 @@ object CollectionEditorRepository {
}
val id = TmdbCollectionSourceResolver.parseTmdbId(state.tmdbInput)
if (sourceType != TmdbCollectionSourceType.DISCOVER && id == null) {
_uiState.value = state.copy(
tmdbSearchError = runBlocking { getString(Res.string.collections_editor_tmdb_invalid_id_or_url) },
)
scope.launch {
_uiState.value = _uiState.value.copy(
tmdbSearchError = getString(Res.string.collections_editor_tmdb_invalid_id_error),
)
}
return
}
val mediaTypes = selectedMediaTypes(state, sourceType)
val baseTitle = state.tmdbTitleInput.ifBlank {
runBlocking {
val idArg = id?.toString().orEmpty()
scope.launch {
val moviesSuffix = getString(Res.string.collections_editor_media_movies_suffix)
val seriesSuffix = getString(Res.string.collections_editor_media_series_suffix)
val baseTitle = state.tmdbTitleInput.ifBlank {
when (sourceType) {
TmdbCollectionSourceType.LIST -> getString(Res.string.collections_editor_tmdb_list_title_format, idArg).trim()
TmdbCollectionSourceType.COLLECTION -> getString(Res.string.collections_editor_tmdb_collection_title_format, idArg).trim()
TmdbCollectionSourceType.COMPANY -> getString(Res.string.collections_editor_tmdb_production_title_format, idArg).trim()
TmdbCollectionSourceType.NETWORK -> getString(Res.string.collections_editor_tmdb_network_title_format, idArg).trim()
TmdbCollectionSourceType.PERSON -> getString(Res.string.collections_editor_tmdb_person_title_format, idArg).trim()
TmdbCollectionSourceType.DIRECTOR -> getString(Res.string.collections_editor_tmdb_director_title_format, idArg).trim()
TmdbCollectionSourceType.DISCOVER -> getString(Res.string.collections_editor_tmdb_discover_title)
TmdbCollectionSourceType.LIST -> "TMDB List ${id ?: ""}".trim()
TmdbCollectionSourceType.COLLECTION -> "TMDB Collection ${id ?: ""}".trim()
TmdbCollectionSourceType.COMPANY -> "TMDB Production ${id ?: ""}".trim()
TmdbCollectionSourceType.NETWORK -> "TMDB Network ${id ?: ""}".trim()
TmdbCollectionSourceType.PERSON -> "TMDB Person ${id ?: ""}".trim()
TmdbCollectionSourceType.DIRECTOR -> "TMDB Director ${id ?: ""}".trim()
TmdbCollectionSourceType.DISCOVER -> getString(Res.string.collections_editor_tmdb_discover)
}
}
}
val sources = mediaTypes.map { mediaType ->
CollectionSource(
provider = "tmdb",
tmdbSourceType = sourceType.name,
title = titleForMedia(baseTitle, mediaType, mediaTypes.size > 1),
tmdbId = id,
mediaType = mediaType.name,
sortBy = state.tmdbSortBy,
filters = state.tmdbFilters,
)
}
if (sourceType == TmdbCollectionSourceType.LIST || sourceType == TmdbCollectionSourceType.COLLECTION) {
scope.launch {
val sources = mediaTypes.map { mediaType ->
CollectionSource(
provider = "tmdb",
tmdbSourceType = sourceType.name,
title = titleForMedia(baseTitle, mediaType, mediaTypes.size > 1, moviesSuffix, seriesSuffix),
tmdbId = id,
mediaType = mediaType.name,
sortBy = state.tmdbSortBy,
filters = state.tmdbFilters,
)
}
if (sourceType == TmdbCollectionSourceType.LIST || sourceType == TmdbCollectionSourceType.COLLECTION) {
val metadata = runCatching { TmdbCollectionSourceResolver.importMetadata(sourceType, id!!) }
val resolved = metadata.getOrNull()
if (metadata.isFailure) {
_uiState.value = _uiState.value.copy(
tmdbSearchError = metadata.exceptionOrNull()?.message
?: getString(Res.string.collections_editor_tmdb_source_load_failed),
?: getString(Res.string.collections_editor_tmdb_load_error),
)
return@launch
}
@ -695,10 +690,10 @@ object CollectionEditorRepository {
},
coverImageUrl = resolved?.coverImageUrl,
)
} else {
addTmdbSourcesFromPicker(sources)
}
return
}
addTmdbSourcesFromPicker(sources)
}
private fun addTmdbSources(sources: List<CollectionSource>, coverImageUrl: String? = null) {
@ -730,20 +725,24 @@ object CollectionEditorRepository {
val state = _uiState.value
val input = state.traktInput.trim()
if (input.isBlank()) {
_uiState.value = state.copy(
traktSearchError = runBlocking { getString(Res.string.collections_editor_trakt_enter_id_or_url) },
)
scope.launch {
_uiState.value = _uiState.value.copy(
traktSearchError = getString(Res.string.collections_editor_trakt_id_url_required),
)
}
return
}
scope.launch {
val moviesSuffix = getString(Res.string.collections_editor_media_movies_suffix)
val seriesSuffix = getString(Res.string.collections_editor_media_series_suffix)
val metadata = runCatching { TraktPublicListSourceResolver.listImportMetadata(input) }
val resolved = metadata.getOrNull()
val listId = resolved?.traktListId
if (metadata.isFailure || listId == null) {
_uiState.value = _uiState.value.copy(
traktSearchError = metadata.exceptionOrNull()?.message
?: getString(Res.string.collections_editor_trakt_load_failed),
?: getString(Res.string.collections_editor_trakt_load_error),
)
return@launch
}
@ -755,7 +754,7 @@ object CollectionEditorRepository {
sources = selectedTraktMediaTypes(state).map { mediaType ->
CollectionSource(
provider = "trakt",
title = titleForMedia(title, mediaType, state.traktMediaBoth),
title = titleForMedia(title, mediaType, state.traktMediaBoth, moviesSuffix, seriesSuffix),
traktListId = listId,
mediaType = mediaType.name,
sortBy = TraktListSort.normalize(state.traktSortBy),
@ -770,19 +769,23 @@ object CollectionEditorRepository {
fun addTraktSourceFromResult(result: TraktPublicListSearchResult) {
val state = _uiState.value
val title = state.traktTitleInput.ifBlank { result.title }
addTraktSourcesToFolder(
sources = selectedTraktMediaTypes(state).map { mediaType ->
CollectionSource(
provider = "trakt",
title = titleForMedia(title, mediaType, state.traktMediaBoth),
traktListId = result.traktListId,
mediaType = mediaType.name,
sortBy = TraktListSort.normalize(state.traktSortBy),
sortHow = TraktSortHow.normalize(state.traktSortHow),
)
},
coverImageUrl = result.coverImageUrl,
)
scope.launch {
val moviesSuffix = getString(Res.string.collections_editor_media_movies_suffix)
val seriesSuffix = getString(Res.string.collections_editor_media_series_suffix)
addTraktSourcesToFolder(
sources = selectedTraktMediaTypes(state).map { mediaType ->
CollectionSource(
provider = "trakt",
title = titleForMedia(title, mediaType, state.traktMediaBoth, moviesSuffix, seriesSuffix),
traktListId = result.traktListId,
mediaType = mediaType.name,
sortBy = TraktListSort.normalize(state.traktSortBy),
sortHow = TraktSortHow.normalize(state.traktSortHow),
)
},
coverImageUrl = result.coverImageUrl,
)
}
}
private fun addTraktSourcesToFolder(sources: List<CollectionSource>, coverImageUrl: String? = null) {
@ -913,15 +916,13 @@ private fun titleForMedia(
title: String,
mediaType: TmdbCollectionMediaType,
addSuffix: Boolean,
moviesSuffix: String,
seriesSuffix: String,
): String {
if (!addSuffix) return title
val suffix = runBlocking {
getString(
when (mediaType) {
TmdbCollectionMediaType.MOVIE -> Res.string.media_movies
TmdbCollectionMediaType.TV -> Res.string.media_series
},
)
val suffix = when (mediaType) {
TmdbCollectionMediaType.MOVIE -> moviesSuffix
TmdbCollectionMediaType.TV -> seriesSuffix
}
return "$title $suffix"
}

View file

@ -1269,7 +1269,7 @@ private fun TmdbSourcePickerScreen(
label = stringResource(Res.string.collections_editor_tmdb_country),
helper = stringResource(Res.string.collections_editor_tmdb_country_helper),
value = state.tmdbFilters.withOriginCountry.orEmpty(),
placeholder = stringResource(Res.string.collections_editor_tmdb_country_placeholder),
placeholder = stringResource(Res.string.collections_editor_watch_region_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withOriginCountry = value.ifBlank { null })
@ -1387,9 +1387,9 @@ private fun TmdbSourcePickerScreen(
chips = listOf(
stringResource(Res.string.collections_editor_tmdb_country_us) to "US",
stringResource(Res.string.collections_editor_tmdb_country_uk) to "GB",
"Canada" to "CA",
"Australia" to "AU",
"Germany" to "DE",
stringResource(Res.string.collections_editor_tmdb_country_ca) to "CA",
stringResource(Res.string.collections_editor_tmdb_country_au) to "AU",
stringResource(Res.string.collections_editor_tmdb_country_de) to "DE",
),
onSelect = { value ->
CollectionEditorRepository.updateTmdbFilters { it.copy(watchRegion = value) }

View file

@ -14,17 +14,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.collections_tmdb_api_key_required
import nuvio.composeapp.generated.resources.collections_tmdb_collection_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_company_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_discover_no_data
import nuvio.composeapp.generated.resources.collections_tmdb_list_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_missing_collection_id
import nuvio.composeapp.generated.resources.collections_tmdb_missing_list_id
import nuvio.composeapp.generated.resources.collections_tmdb_missing_person_id
import nuvio.composeapp.generated.resources.collections_tmdb_network_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_person_credits_not_found
import nuvio.composeapp.generated.resources.collections_tmdb_person_not_found
import nuvio.composeapp.generated.resources.collections_editor_tmdb_discover
import org.jetbrains.compose.resources.getString
import kotlin.math.roundToInt
@ -113,7 +103,7 @@ object TmdbCollectionSourceResolver {
)
}
TmdbCollectionSourceType.DISCOVER -> TmdbSourceImportMetadata(title = "TMDB Discover")
TmdbCollectionSourceType.DISCOVER -> TmdbSourceImportMetadata(title = getString(Res.string.collections_editor_tmdb_discover))
}
}

View file

@ -41,6 +41,22 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.player_video_settings_brightness
import nuvio.composeapp.generated.resources.player_video_settings_contrast
import nuvio.composeapp.generated.resources.player_video_settings_deband
import nuvio.composeapp.generated.resources.player_video_settings_deband_desc
import nuvio.composeapp.generated.resources.player_video_settings_gamma
import nuvio.composeapp.generated.resources.player_video_settings_hdr_peak_detection
import nuvio.composeapp.generated.resources.player_video_settings_hdr_peak_detection_desc
import nuvio.composeapp.generated.resources.player_video_settings_interpolation
import nuvio.composeapp.generated.resources.player_video_settings_interpolation_desc
import nuvio.composeapp.generated.resources.player_video_settings_output_preset
import nuvio.composeapp.generated.resources.player_video_settings_reset_tuning
import nuvio.composeapp.generated.resources.player_video_settings_saturation
import nuvio.composeapp.generated.resources.player_video_settings_title
import nuvio.composeapp.generated.resources.player_video_settings_tone_mapping
import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToInt
@Composable
@ -96,7 +112,7 @@ internal fun IosVideoSettingsModal(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Video",
text = stringResource(Res.string.player_video_settings_title),
color = colorScheme.onSurface,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
@ -106,7 +122,7 @@ internal fun IosVideoSettingsModal(
PlayerSettingsRepository.resetIosVideoOutputTuning()
onSettingsChanged()
}) {
Text("Reset tuning")
Text(stringResource(Res.string.player_video_settings_reset_tuning))
}
}
@ -118,11 +134,11 @@ internal fun IosVideoSettingsModal(
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
OptionGroup(
title = "Output preset",
title = stringResource(Res.string.player_video_settings_output_preset),
options = IosVideoOutputPreset.entries,
selected = settings.iosVideoOutputPreset,
label = { it.label },
description = { it.description },
label = { it.localizedLabel() },
description = { it.localizedDescription() },
onSelect = {
PlayerSettingsRepository.setIosVideoOutputPreset(it)
onSettingsChanged()
@ -130,8 +146,8 @@ internal fun IosVideoSettingsModal(
)
ToggleRow(
title = "HDR peak detection",
description = "Estimate HDR peak brightness when metadata is bad or missing.",
title = stringResource(Res.string.player_video_settings_hdr_peak_detection),
description = stringResource(Res.string.player_video_settings_hdr_peak_detection_desc),
checked = settings.iosHdrComputePeakEnabled,
onCheckedChange = {
PlayerSettingsRepository.setIosHdrComputePeakEnabled(it)
@ -140,7 +156,7 @@ internal fun IosVideoSettingsModal(
)
OptionGroup(
title = "Tone mapping",
title = stringResource(Res.string.player_video_settings_tone_mapping),
options = IosToneMappingMode.entries,
selected = settings.iosToneMappingMode,
label = { it.label },
@ -151,8 +167,8 @@ internal fun IosVideoSettingsModal(
)
ToggleRow(
title = "Deband",
description = "Reduce color banding at a small performance cost.",
title = stringResource(Res.string.player_video_settings_deband),
description = stringResource(Res.string.player_video_settings_deband_desc),
checked = settings.iosDebandEnabled,
onCheckedChange = {
PlayerSettingsRepository.setIosDebandEnabled(it)
@ -160,8 +176,8 @@ internal fun IosVideoSettingsModal(
},
)
ToggleRow(
title = "Frame interpolation",
description = "Smooth motion when mpv can use display sync cleanly.",
title = stringResource(Res.string.player_video_settings_interpolation),
description = stringResource(Res.string.player_video_settings_interpolation_desc),
checked = settings.iosInterpolationEnabled,
onCheckedChange = {
PlayerSettingsRepository.setIosInterpolationEnabled(it)
@ -170,7 +186,7 @@ internal fun IosVideoSettingsModal(
)
PictureSlider(
title = "Brightness",
title = stringResource(Res.string.player_video_settings_brightness),
value = settings.iosBrightness,
onValueChanged = {
PlayerSettingsRepository.setIosBrightness(it)
@ -178,7 +194,7 @@ internal fun IosVideoSettingsModal(
},
)
PictureSlider(
title = "Contrast",
title = stringResource(Res.string.player_video_settings_contrast),
value = settings.iosContrast,
onValueChanged = {
PlayerSettingsRepository.setIosContrast(it)
@ -186,7 +202,7 @@ internal fun IosVideoSettingsModal(
},
)
PictureSlider(
title = "Saturation",
title = stringResource(Res.string.player_video_settings_saturation),
value = settings.iosSaturation,
onValueChanged = {
PlayerSettingsRepository.setIosSaturation(it)
@ -194,7 +210,7 @@ internal fun IosVideoSettingsModal(
},
)
PictureSlider(
title = "Gamma",
title = stringResource(Res.string.player_video_settings_gamma),
value = settings.iosGamma,
onValueChanged = {
PlayerSettingsRepository.setIosGamma(it)
@ -260,8 +276,8 @@ private fun <T> OptionGroup(
title: String,
options: List<T>,
selected: T,
label: (T) -> String,
description: ((T) -> String)? = null,
label: @Composable (T) -> String,
description: @Composable ((T) -> String)? = null,
onSelect: (T) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {

View file

@ -308,7 +308,7 @@ private fun PlayerHeader(
if (onSubmitIntroClick != null) {
PlayerHeaderIconButton(
icon = Icons.Rounded.Flag,
contentDescription = stringResource(Res.string.submit_intro_action),
contentDescription = stringResource(Res.string.player_action_submit_intro),
buttonSize = metrics.headerIconSize + 16.dp,
iconSize = metrics.headerIconSize,
onClick = onSubmitIntroClick,
@ -328,7 +328,7 @@ private fun PlayerHeader(
if (onVideoSettingsClick != null) {
PlayerHeaderIconButton(
icon = Icons.Rounded.Build,
contentDescription = "Video settings",
contentDescription = stringResource(Res.string.player_action_video_settings),
buttonSize = metrics.headerIconSize + 16.dp,
iconSize = metrics.headerIconSize,
onClick = onVideoSettingsClick,

View file

@ -1,6 +1,18 @@
package com.nuvio.app.features.player
import androidx.compose.runtime.Composable
import kotlinx.serialization.Serializable
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.player_ios_hardware_decoder_off
import nuvio.composeapp.generated.resources.player_ios_preset_compatibility_desc
import nuvio.composeapp.generated.resources.player_ios_preset_compatibility_label
import nuvio.composeapp.generated.resources.player_ios_preset_custom_desc
import nuvio.composeapp.generated.resources.player_ios_preset_custom_label
import nuvio.composeapp.generated.resources.player_ios_preset_native_edr_desc
import nuvio.composeapp.generated.resources.player_ios_preset_native_edr_label
import nuvio.composeapp.generated.resources.player_ios_preset_sdr_tone_mapped_desc
import nuvio.composeapp.generated.resources.player_ios_preset_sdr_tone_mapped_label
import org.jetbrains.compose.resources.stringResource
@Serializable
data class PlayerRoute(
@ -129,6 +141,28 @@ enum class IosHardwareDecoderMode(
Off("no", "Off"),
}
@Composable
fun IosVideoOutputPreset.localizedLabel(): String = when (this) {
IosVideoOutputPreset.NativeEdr -> stringResource(Res.string.player_ios_preset_native_edr_label)
IosVideoOutputPreset.SdrToneMapped -> stringResource(Res.string.player_ios_preset_sdr_tone_mapped_label)
IosVideoOutputPreset.Compatibility -> stringResource(Res.string.player_ios_preset_compatibility_label)
IosVideoOutputPreset.Custom -> stringResource(Res.string.player_ios_preset_custom_label)
}
@Composable
fun IosVideoOutputPreset.localizedDescription(): String = when (this) {
IosVideoOutputPreset.NativeEdr -> stringResource(Res.string.player_ios_preset_native_edr_desc)
IosVideoOutputPreset.SdrToneMapped -> stringResource(Res.string.player_ios_preset_sdr_tone_mapped_desc)
IosVideoOutputPreset.Compatibility -> stringResource(Res.string.player_ios_preset_compatibility_desc)
IosVideoOutputPreset.Custom -> stringResource(Res.string.player_ios_preset_custom_desc)
}
@Composable
fun IosHardwareDecoderMode.localizedLabel(): String = when (this) {
IosHardwareDecoderMode.Off -> stringResource(Res.string.player_ios_hardware_decoder_off)
else -> label
}
data class PlayerPlaybackSnapshot(
val isLoading: Boolean = true,
val isPlaying: Boolean = false,

View file

@ -52,14 +52,14 @@ import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.action_cancel
import nuvio.composeapp.generated.resources.action_close
import nuvio.composeapp.generated.resources.submit_intro_button_submit
import nuvio.composeapp.generated.resources.submit_intro_capture_button
import nuvio.composeapp.generated.resources.submit_intro_end_time_label
import nuvio.composeapp.generated.resources.submit_intro_capture
import nuvio.composeapp.generated.resources.submit_intro_end_time
import nuvio.composeapp.generated.resources.submit_intro_segment_intro
import nuvio.composeapp.generated.resources.submit_intro_segment_outro
import nuvio.composeapp.generated.resources.submit_intro_segment_recap
import nuvio.composeapp.generated.resources.submit_intro_segment_type_label
import nuvio.composeapp.generated.resources.submit_intro_start_time_label
import nuvio.composeapp.generated.resources.submit_intro_segment_type
import nuvio.composeapp.generated.resources.submit_intro_start_time
import nuvio.composeapp.generated.resources.submit_intro_submit
import nuvio.composeapp.generated.resources.submit_intro_title
import org.jetbrains.compose.resources.stringResource
import kotlin.math.floor
@ -110,18 +110,14 @@ fun SubmitIntroDialog(
fontWeight = FontWeight.Bold,
)
IconButton(onClick = onDismiss) {
Icon(
Icons.Rounded.Close,
contentDescription = stringResource(Res.string.action_close),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Icon(Icons.Rounded.Close, contentDescription = stringResource(Res.string.action_close), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
// Segment Type
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(Res.string.submit_intro_segment_type_label),
text = stringResource(Res.string.submit_intro_segment_type),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
@ -156,7 +152,7 @@ fun SubmitIntroDialog(
// Start Time
TimeInputRow(
label = stringResource(Res.string.submit_intro_start_time_label),
label = stringResource(Res.string.submit_intro_start_time),
value = startTimeStr,
onValueChange = onStartTimeChange,
onCapture = { onStartTimeChange(formatSecondsToMMSS(currentTimeSec)) }
@ -164,7 +160,7 @@ fun SubmitIntroDialog(
// End Time
TimeInputRow(
label = stringResource(Res.string.submit_intro_end_time_label),
label = stringResource(Res.string.submit_intro_end_time),
value = endTimeStr,
onValueChange = onEndTimeChange,
onCapture = { onEndTimeChange(formatSecondsToMMSS(currentTimeSec)) }
@ -234,7 +230,7 @@ fun SubmitIntroDialog(
) {
Icon(Icons.Rounded.Send, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(18.dp))
Text(
text = stringResource(Res.string.submit_intro_button_submit),
text = stringResource(Res.string.submit_intro_submit),
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Bold
)
@ -345,7 +341,7 @@ private fun TimeInputRow(
modifier = Modifier.size(18.dp)
)
Text(
text = stringResource(Res.string.submit_intro_capture_button),
text = stringResource(Res.string.submit_intro_capture),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold

View file

@ -120,8 +120,82 @@ import nuvio.composeapp.generated.resources.settings_debrid_resolve_with_descrip
import nuvio.composeapp.generated.resources.settings_debrid_section_instant_playback
import nuvio.composeapp.generated.resources.settings_debrid_section_formatting
import nuvio.composeapp.generated.resources.settings_debrid_section_providers
import nuvio.composeapp.generated.resources.settings_debrid_section_result_management
import nuvio.composeapp.generated.resources.settings_debrid_section_title
import nuvio.composeapp.generated.resources.settings_debrid_max_results
import nuvio.composeapp.generated.resources.settings_debrid_max_results_desc
import nuvio.composeapp.generated.resources.settings_debrid_sort_results
import nuvio.composeapp.generated.resources.settings_debrid_sort_results_desc
import nuvio.composeapp.generated.resources.settings_debrid_per_resolution_limit
import nuvio.composeapp.generated.resources.settings_debrid_per_resolution_limit_desc
import nuvio.composeapp.generated.resources.settings_debrid_per_quality_limit
import nuvio.composeapp.generated.resources.settings_debrid_per_quality_limit_desc
import nuvio.composeapp.generated.resources.settings_debrid_size_range
import nuvio.composeapp.generated.resources.settings_debrid_size_range_desc
import nuvio.composeapp.generated.resources.settings_debrid_learn_more
import nuvio.composeapp.generated.resources.settings_debrid_template_default_format
import nuvio.composeapp.generated.resources.settings_debrid_release_groups_hint
import nuvio.composeapp.generated.resources.settings_debrid_sort_default
import nuvio.composeapp.generated.resources.settings_debrid_sort_largest
import nuvio.composeapp.generated.resources.settings_debrid_sort_smallest
import nuvio.composeapp.generated.resources.settings_debrid_sort_best_audio
import nuvio.composeapp.generated.resources.settings_debrid_sort_language
import nuvio.composeapp.generated.resources.settings_debrid_selection_any
import nuvio.composeapp.generated.resources.settings_debrid_selection_count
import nuvio.composeapp.generated.resources.settings_debrid_results_all
import nuvio.composeapp.generated.resources.settings_debrid_results_count
import nuvio.composeapp.generated.resources.settings_debrid_size_up_to
import nuvio.composeapp.generated.resources.settings_debrid_size_min
import nuvio.composeapp.generated.resources.settings_debrid_size_range_value
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_resolutions
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_resolutions_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_resolutions
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_resolutions_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_resolutions
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_resolutions_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_qualities
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_qualities_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_qualities
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_qualities_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_qualities
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_qualities_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_visual_tags
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_visual_tags_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_visual_tags
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_visual_tags_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_visual_tags
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_visual_tags_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_audio_tags
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_audio_tags_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_audio_tags
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_audio_tags_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_audio_tags
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_audio_tags_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_channels
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_channels_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_channels
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_channels_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_channels
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_channels_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_encodes
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_encodes_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_encodes
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_encodes_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_encodes
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_encodes_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_languages
import nuvio.composeapp.generated.resources.settings_debrid_rule_preferred_languages_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_languages
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_languages_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_languages
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_languages_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_release_groups
import nuvio.composeapp.generated.resources.settings_debrid_rule_required_release_groups_desc
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_release_groups
import nuvio.composeapp.generated.resources.settings_debrid_rule_excluded_release_groups_desc
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import kotlinx.coroutines.runBlocking
private const val CLOUD_SERVICES_FAQ_URL = "https://nuvioapp.space/faq#common-cloud-library-and-cloud-services"
@ -319,14 +393,14 @@ internal fun LazyListScope.debridSettingsContent(
val rows = debridRuleRows(preferences)
SettingsSection(
title = "Result Management",
title = stringResource(Res.string.settings_debrid_section_result_management),
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
DebridPreferenceRow(
isTablet = isTablet,
title = "Max results",
description = "Limit how many results appear.",
title = stringResource(Res.string.settings_debrid_max_results),
description = stringResource(Res.string.settings_debrid_max_results_desc),
value = streamMaxResultsLabel(preferences.maxResults),
enabled = settings.canResolvePlayableLinks,
onClick = { activeStreamPicker = DebridStreamPicker.MAX_RESULTS },
@ -334,8 +408,8 @@ internal fun LazyListScope.debridSettingsContent(
SettingsGroupDivider(isTablet = isTablet)
DebridPreferenceRow(
isTablet = isTablet,
title = "Sort results",
description = "Choose how results are ordered.",
title = stringResource(Res.string.settings_debrid_sort_results),
description = stringResource(Res.string.settings_debrid_sort_results_desc),
value = sortProfileLabel(preferences.sortCriteria),
enabled = settings.canResolvePlayableLinks,
onClick = { activeStreamPicker = DebridStreamPicker.SORT_MODE },
@ -343,8 +417,8 @@ internal fun LazyListScope.debridSettingsContent(
SettingsGroupDivider(isTablet = isTablet)
DebridPreferenceRow(
isTablet = isTablet,
title = "Per resolution limit",
description = "Cap repeated 2160p, 1080p, 720p results after sorting.",
title = stringResource(Res.string.settings_debrid_per_resolution_limit),
description = stringResource(Res.string.settings_debrid_per_resolution_limit_desc),
value = streamMaxResultsLabel(preferences.maxPerResolution),
enabled = settings.canResolvePlayableLinks,
onClick = { activeStreamPicker = DebridStreamPicker.MAX_PER_RESOLUTION },
@ -352,8 +426,8 @@ internal fun LazyListScope.debridSettingsContent(
SettingsGroupDivider(isTablet = isTablet)
DebridPreferenceRow(
isTablet = isTablet,
title = "Per quality limit",
description = "Cap repeated BluRay, WEB-DL, REMUX results after sorting.",
title = stringResource(Res.string.settings_debrid_per_quality_limit),
description = stringResource(Res.string.settings_debrid_per_quality_limit_desc),
value = streamMaxResultsLabel(preferences.maxPerQuality),
enabled = settings.canResolvePlayableLinks,
onClick = { activeStreamPicker = DebridStreamPicker.MAX_PER_QUALITY },
@ -361,8 +435,8 @@ internal fun LazyListScope.debridSettingsContent(
SettingsGroupDivider(isTablet = isTablet)
DebridPreferenceRow(
isTablet = isTablet,
title = "Size range",
description = "Filter results by file size.",
title = stringResource(Res.string.settings_debrid_size_range),
description = stringResource(Res.string.settings_debrid_size_range_desc),
value = sizeRangeLabel(preferences),
enabled = settings.canResolvePlayableLinks,
onClick = { activeStreamPicker = DebridStreamPicker.SIZE_RANGE },
@ -480,7 +554,7 @@ private fun DebridLearnMoreFooter(
contentAlignment = Alignment.Center,
) {
TextButton(onClick = onClick) {
Text("Learn more")
Text(stringResource(Res.string.settings_debrid_learn_more))
}
}
}
@ -491,12 +565,13 @@ private enum class DebridTemplateField {
}
private fun templatePreview(value: String, defaultValue: String): String {
if (value.trim().isBlank() || value.trim() == defaultValue.trim()) return "Default format"
val defaultFormat = runBlocking { getString(Res.string.settings_debrid_template_default_format) }
if (value.trim().isBlank() || value.trim() == defaultValue.trim()) return defaultFormat
val firstLine = value
.lineSequence()
.map { it.trim() }
.firstOrNull { it.isNotBlank() }
?: return "Default format"
?: return defaultFormat
return if (firstLine.length <= 28) firstLine else "${firstLine.take(28)}..."
}
@ -716,7 +791,7 @@ private fun DebridStreamPreferenceDialog(
) {
when (picker) {
DebridStreamPicker.MAX_RESULTS -> DebridIntChoiceDialog(
title = "Max results",
title = stringResource(Res.string.settings_debrid_max_results),
selectedValue = preferences.maxResults,
options = listOf(0, 5, 10, 20, 50),
label = { streamMaxResultsLabel(it) },
@ -724,7 +799,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.MAX_PER_RESOLUTION -> DebridIntChoiceDialog(
title = "Max results",
title = stringResource(Res.string.settings_debrid_max_results),
selectedValue = preferences.maxPerResolution,
options = listOf(0, 1, 2, 3, 5),
label = { streamMaxResultsLabel(it) },
@ -732,7 +807,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.MAX_PER_QUALITY -> DebridIntChoiceDialog(
title = "Max results",
title = stringResource(Res.string.settings_debrid_max_results),
selectedValue = preferences.maxPerQuality,
options = listOf(0, 1, 2, 3, 5),
label = { streamMaxResultsLabel(it) },
@ -740,7 +815,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.SORT_MODE -> DebridSingleChoiceDialog(
title = "Sort results",
title = stringResource(Res.string.settings_debrid_sort_results),
selectedValue = sortProfileFor(preferences.sortCriteria),
options = listOf(
DebridSortProfile.DEFAULT,
@ -754,7 +829,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.SIZE_RANGE -> DebridSingleChoiceDialog(
title = "Size range",
title = stringResource(Res.string.settings_debrid_size_range),
selectedValue = preferences.sizeMinGb to preferences.sizeMaxGb,
options = listOf(0 to 0, 0 to 5, 0 to 10, 5 to 20, 10 to 50, 20 to 100),
label = { sizeRangeLabel(it.first, it.second) },
@ -762,7 +837,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.PREFERRED_RESOLUTIONS -> DebridMultiChoiceDialog(
title = "Preferred resolutions",
title = stringResource(Res.string.settings_debrid_rule_preferred_resolutions),
selectedValues = preferences.preferredResolutions,
values = DebridStreamResolution.defaultOrder,
label = { it.label },
@ -770,7 +845,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_RESOLUTIONS -> DebridMultiChoiceDialog(
title = "Required resolutions",
title = stringResource(Res.string.settings_debrid_rule_required_resolutions),
selectedValues = preferences.requiredResolutions,
values = DebridStreamResolution.defaultOrder,
label = { it.label },
@ -778,7 +853,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_RESOLUTIONS -> DebridMultiChoiceDialog(
title = "Excluded resolutions",
title = stringResource(Res.string.settings_debrid_rule_excluded_resolutions),
selectedValues = preferences.excludedResolutions,
values = DebridStreamResolution.defaultOrder,
label = { it.label },
@ -786,7 +861,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.PREFERRED_QUALITIES -> DebridMultiChoiceDialog(
title = "Preferred qualities",
title = stringResource(Res.string.settings_debrid_rule_preferred_qualities),
selectedValues = preferences.preferredQualities,
values = DebridStreamQuality.defaultOrder,
label = { it.label },
@ -794,7 +869,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_QUALITIES -> DebridMultiChoiceDialog(
title = "Required qualities",
title = stringResource(Res.string.settings_debrid_rule_required_qualities),
selectedValues = preferences.requiredQualities,
values = DebridStreamQuality.defaultOrder,
label = { it.label },
@ -802,7 +877,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_QUALITIES -> DebridMultiChoiceDialog(
title = "Excluded qualities",
title = stringResource(Res.string.settings_debrid_rule_excluded_qualities),
selectedValues = preferences.excludedQualities,
values = DebridStreamQuality.defaultOrder,
label = { it.label },
@ -810,7 +885,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.PREFERRED_VISUAL_TAGS -> DebridMultiChoiceDialog(
title = "Preferred visual tags",
title = stringResource(Res.string.settings_debrid_rule_preferred_visual_tags),
selectedValues = preferences.preferredVisualTags,
values = DebridStreamVisualTag.defaultOrder,
label = { it.label },
@ -818,7 +893,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_VISUAL_TAGS -> DebridMultiChoiceDialog(
title = "Required visual tags",
title = stringResource(Res.string.settings_debrid_rule_required_visual_tags),
selectedValues = preferences.requiredVisualTags,
values = DebridStreamVisualTag.defaultOrder,
label = { it.label },
@ -826,7 +901,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_VISUAL_TAGS -> DebridMultiChoiceDialog(
title = "Excluded visual tags",
title = stringResource(Res.string.settings_debrid_rule_excluded_visual_tags),
selectedValues = preferences.excludedVisualTags,
values = DebridStreamVisualTag.defaultOrder,
label = { it.label },
@ -834,7 +909,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.PREFERRED_AUDIO_TAGS -> DebridMultiChoiceDialog(
title = "Preferred audio tags",
title = stringResource(Res.string.settings_debrid_rule_preferred_audio_tags),
selectedValues = preferences.preferredAudioTags,
values = DebridStreamAudioTag.defaultOrder,
label = { it.label },
@ -842,7 +917,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_AUDIO_TAGS -> DebridMultiChoiceDialog(
title = "Required audio tags",
title = stringResource(Res.string.settings_debrid_rule_required_audio_tags),
selectedValues = preferences.requiredAudioTags,
values = DebridStreamAudioTag.defaultOrder,
label = { it.label },
@ -850,7 +925,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_AUDIO_TAGS -> DebridMultiChoiceDialog(
title = "Excluded audio tags",
title = stringResource(Res.string.settings_debrid_rule_excluded_audio_tags),
selectedValues = preferences.excludedAudioTags,
values = DebridStreamAudioTag.defaultOrder,
label = { it.label },
@ -858,7 +933,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.PREFERRED_AUDIO_CHANNELS -> DebridMultiChoiceDialog(
title = "Preferred channels",
title = stringResource(Res.string.settings_debrid_rule_preferred_channels),
selectedValues = preferences.preferredAudioChannels,
values = DebridStreamAudioChannel.defaultOrder,
label = { it.label },
@ -866,7 +941,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_AUDIO_CHANNELS -> DebridMultiChoiceDialog(
title = "Required channels",
title = stringResource(Res.string.settings_debrid_rule_required_channels),
selectedValues = preferences.requiredAudioChannels,
values = DebridStreamAudioChannel.defaultOrder,
label = { it.label },
@ -874,7 +949,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_AUDIO_CHANNELS -> DebridMultiChoiceDialog(
title = "Excluded channels",
title = stringResource(Res.string.settings_debrid_rule_excluded_channels),
selectedValues = preferences.excludedAudioChannels,
values = DebridStreamAudioChannel.defaultOrder,
label = { it.label },
@ -882,7 +957,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.PREFERRED_ENCODES -> DebridMultiChoiceDialog(
title = "Preferred encodes",
title = stringResource(Res.string.settings_debrid_rule_preferred_encodes),
selectedValues = preferences.preferredEncodes,
values = DebridStreamEncode.defaultOrder,
label = { it.label },
@ -890,7 +965,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_ENCODES -> DebridMultiChoiceDialog(
title = "Required encodes",
title = stringResource(Res.string.settings_debrid_rule_required_encodes),
selectedValues = preferences.requiredEncodes,
values = DebridStreamEncode.defaultOrder,
label = { it.label },
@ -898,7 +973,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_ENCODES -> DebridMultiChoiceDialog(
title = "Excluded encodes",
title = stringResource(Res.string.settings_debrid_rule_excluded_encodes),
selectedValues = preferences.excludedEncodes,
values = DebridStreamEncode.defaultOrder,
label = { it.label },
@ -906,7 +981,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.PREFERRED_LANGUAGES -> DebridMultiChoiceDialog(
title = "Preferred languages",
title = stringResource(Res.string.settings_debrid_rule_preferred_languages),
selectedValues = preferences.preferredLanguages,
values = DebridStreamLanguage.entries,
label = { it.label },
@ -914,7 +989,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_LANGUAGES -> DebridMultiChoiceDialog(
title = "Required languages",
title = stringResource(Res.string.settings_debrid_rule_required_languages),
selectedValues = preferences.requiredLanguages,
values = DebridStreamLanguage.entries,
label = { it.label },
@ -922,7 +997,7 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_LANGUAGES -> DebridMultiChoiceDialog(
title = "Excluded languages",
title = stringResource(Res.string.settings_debrid_rule_excluded_languages),
selectedValues = preferences.excludedLanguages,
values = DebridStreamLanguage.entries,
label = { it.label },
@ -930,13 +1005,13 @@ private fun DebridStreamPreferenceDialog(
onDismiss = onDismiss,
)
DebridStreamPicker.REQUIRED_RELEASE_GROUPS -> DebridTextListDialog(
title = "Required release groups",
title = stringResource(Res.string.settings_debrid_rule_required_release_groups),
selectedValues = preferences.requiredReleaseGroups,
onSelected = { value -> onPreferencesChanged(preferences.copy(requiredReleaseGroups = value)) },
onDismiss = onDismiss,
)
DebridStreamPicker.EXCLUDED_RELEASE_GROUPS -> DebridTextListDialog(
title = "Excluded release groups",
title = stringResource(Res.string.settings_debrid_rule_excluded_release_groups),
selectedValues = preferences.excludedReleaseGroups,
onSelected = { value -> onPreferencesChanged(preferences.copy(excludedReleaseGroups = value)) },
onDismiss = onDismiss,
@ -1063,7 +1138,7 @@ private fun DebridTextListDialog(
BasicAlertDialog(onDismissRequest = onDismiss) {
DebridDialogSurface(title = title) {
Text(
text = "Enter one group per line.",
text = stringResource(Res.string.settings_debrid_release_groups_hint),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -1184,56 +1259,73 @@ private fun DebridDialogOptionRow(
@Composable
private fun streamMaxResultsLabel(value: Int): String =
if (value <= 0) "All results" else "$value results"
private fun sortProfileLabel(value: DebridSortProfile): String =
when (value) {
DebridSortProfile.DEFAULT -> "Default"
DebridSortProfile.LARGEST -> "Largest first"
DebridSortProfile.SMALLEST -> "Smallest first"
DebridSortProfile.AUDIO -> "Best audio first"
DebridSortProfile.LANGUAGE -> "Language first"
if (value <= 0) {
stringResource(Res.string.settings_debrid_results_all)
} else {
stringResource(Res.string.settings_debrid_results_count, value)
}
@Composable
private fun sortProfileLabel(value: DebridSortProfile): String =
when (value) {
DebridSortProfile.DEFAULT -> stringResource(Res.string.settings_debrid_sort_default)
DebridSortProfile.LARGEST -> stringResource(Res.string.settings_debrid_sort_largest)
DebridSortProfile.SMALLEST -> stringResource(Res.string.settings_debrid_sort_smallest)
DebridSortProfile.AUDIO -> stringResource(Res.string.settings_debrid_sort_best_audio)
DebridSortProfile.LANGUAGE -> stringResource(Res.string.settings_debrid_sort_language)
}
@Composable
private fun sortProfileLabel(criteria: List<DebridStreamSortCriterion>): String =
sortProfileLabel(sortProfileFor(criteria))
@Composable
private fun debridRuleRows(preferences: DebridStreamPreferences): List<DebridRuleRow> =
listOf(
DebridRuleRow(DebridStreamPicker.PREFERRED_RESOLUTIONS, "Preferred resolutions", "Sort selected resolutions first, in default order.", selectionCountLabel(preferences.preferredResolutions)),
DebridRuleRow(DebridStreamPicker.REQUIRED_RESOLUTIONS, "Required resolutions", "Only show selected resolutions.", selectionCountLabel(preferences.requiredResolutions)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_RESOLUTIONS, "Excluded resolutions", "Hide selected resolutions.", selectionCountLabel(preferences.excludedResolutions)),
DebridRuleRow(DebridStreamPicker.PREFERRED_QUALITIES, "Preferred qualities", "Sort selected qualities first, in default order.", selectionCountLabel(preferences.preferredQualities)),
DebridRuleRow(DebridStreamPicker.REQUIRED_QUALITIES, "Required qualities", "Only show selected qualities.", selectionCountLabel(preferences.requiredQualities)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_QUALITIES, "Excluded qualities", "Hide selected qualities.", selectionCountLabel(preferences.excludedQualities)),
DebridRuleRow(DebridStreamPicker.PREFERRED_VISUAL_TAGS, "Preferred visual tags", "Sort DV, HDR, 10bit, IMAX and similar tags.", selectionCountLabel(preferences.preferredVisualTags)),
DebridRuleRow(DebridStreamPicker.REQUIRED_VISUAL_TAGS, "Required visual tags", "Require DV, HDR, 10bit, IMAX, SDR and similar tags.", selectionCountLabel(preferences.requiredVisualTags)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_VISUAL_TAGS, "Excluded visual tags", "Hide DV, HDR, 10bit, 3D and similar tags.", selectionCountLabel(preferences.excludedVisualTags)),
DebridRuleRow(DebridStreamPicker.PREFERRED_AUDIO_TAGS, "Preferred audio tags", "Sort Atmos, TrueHD, DTS, AAC and similar tags.", selectionCountLabel(preferences.preferredAudioTags)),
DebridRuleRow(DebridStreamPicker.REQUIRED_AUDIO_TAGS, "Required audio tags", "Require Atmos, TrueHD, DTS, AAC and similar tags.", selectionCountLabel(preferences.requiredAudioTags)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_AUDIO_TAGS, "Excluded audio tags", "Hide selected audio tags.", selectionCountLabel(preferences.excludedAudioTags)),
DebridRuleRow(DebridStreamPicker.PREFERRED_AUDIO_CHANNELS, "Preferred channels", "Sort preferred channel layouts first.", selectionCountLabel(preferences.preferredAudioChannels)),
DebridRuleRow(DebridStreamPicker.REQUIRED_AUDIO_CHANNELS, "Required channels", "Only show selected channel layouts.", selectionCountLabel(preferences.requiredAudioChannels)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_AUDIO_CHANNELS, "Excluded channels", "Hide selected channel layouts.", selectionCountLabel(preferences.excludedAudioChannels)),
DebridRuleRow(DebridStreamPicker.PREFERRED_ENCODES, "Preferred encodes", "Sort AV1, HEVC, AVC and similar encodes.", selectionCountLabel(preferences.preferredEncodes)),
DebridRuleRow(DebridStreamPicker.REQUIRED_ENCODES, "Required encodes", "Require AV1, HEVC, AVC and similar encodes.", selectionCountLabel(preferences.requiredEncodes)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_ENCODES, "Excluded encodes", "Hide selected encodes.", selectionCountLabel(preferences.excludedEncodes)),
DebridRuleRow(DebridStreamPicker.PREFERRED_LANGUAGES, "Preferred languages", "Sort preferred audio languages first.", selectionCountLabel(preferences.preferredLanguages)),
DebridRuleRow(DebridStreamPicker.REQUIRED_LANGUAGES, "Required languages", "Only show results with selected languages.", selectionCountLabel(preferences.requiredLanguages)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_LANGUAGES, "Excluded languages", "Hide results where every language is excluded.", selectionCountLabel(preferences.excludedLanguages)),
DebridRuleRow(DebridStreamPicker.REQUIRED_RELEASE_GROUPS, "Required release groups", "Only show selected release groups.", selectionCountLabel(preferences.requiredReleaseGroups)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_RELEASE_GROUPS, "Excluded release groups", "Hide selected release groups.", selectionCountLabel(preferences.excludedReleaseGroups)),
DebridRuleRow(DebridStreamPicker.PREFERRED_RESOLUTIONS, stringResource(Res.string.settings_debrid_rule_preferred_resolutions), stringResource(Res.string.settings_debrid_rule_preferred_resolutions_desc), selectionCountLabel(preferences.preferredResolutions)),
DebridRuleRow(DebridStreamPicker.REQUIRED_RESOLUTIONS, stringResource(Res.string.settings_debrid_rule_required_resolutions), stringResource(Res.string.settings_debrid_rule_required_resolutions_desc), selectionCountLabel(preferences.requiredResolutions)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_RESOLUTIONS, stringResource(Res.string.settings_debrid_rule_excluded_resolutions), stringResource(Res.string.settings_debrid_rule_excluded_resolutions_desc), selectionCountLabel(preferences.excludedResolutions)),
DebridRuleRow(DebridStreamPicker.PREFERRED_QUALITIES, stringResource(Res.string.settings_debrid_rule_preferred_qualities), stringResource(Res.string.settings_debrid_rule_preferred_qualities_desc), selectionCountLabel(preferences.preferredQualities)),
DebridRuleRow(DebridStreamPicker.REQUIRED_QUALITIES, stringResource(Res.string.settings_debrid_rule_required_qualities), stringResource(Res.string.settings_debrid_rule_required_qualities_desc), selectionCountLabel(preferences.requiredQualities)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_QUALITIES, stringResource(Res.string.settings_debrid_rule_excluded_qualities), stringResource(Res.string.settings_debrid_rule_excluded_qualities_desc), selectionCountLabel(preferences.excludedQualities)),
DebridRuleRow(DebridStreamPicker.PREFERRED_VISUAL_TAGS, stringResource(Res.string.settings_debrid_rule_preferred_visual_tags), stringResource(Res.string.settings_debrid_rule_preferred_visual_tags_desc), selectionCountLabel(preferences.preferredVisualTags)),
DebridRuleRow(DebridStreamPicker.REQUIRED_VISUAL_TAGS, stringResource(Res.string.settings_debrid_rule_required_visual_tags), stringResource(Res.string.settings_debrid_rule_required_visual_tags_desc), selectionCountLabel(preferences.requiredVisualTags)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_VISUAL_TAGS, stringResource(Res.string.settings_debrid_rule_excluded_visual_tags), stringResource(Res.string.settings_debrid_rule_excluded_visual_tags_desc), selectionCountLabel(preferences.excludedVisualTags)),
DebridRuleRow(DebridStreamPicker.PREFERRED_AUDIO_TAGS, stringResource(Res.string.settings_debrid_rule_preferred_audio_tags), stringResource(Res.string.settings_debrid_rule_preferred_audio_tags_desc), selectionCountLabel(preferences.preferredAudioTags)),
DebridRuleRow(DebridStreamPicker.REQUIRED_AUDIO_TAGS, stringResource(Res.string.settings_debrid_rule_required_audio_tags), stringResource(Res.string.settings_debrid_rule_required_audio_tags_desc), selectionCountLabel(preferences.requiredAudioTags)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_AUDIO_TAGS, stringResource(Res.string.settings_debrid_rule_excluded_audio_tags), stringResource(Res.string.settings_debrid_rule_excluded_audio_tags_desc), selectionCountLabel(preferences.excludedAudioTags)),
DebridRuleRow(DebridStreamPicker.PREFERRED_AUDIO_CHANNELS, stringResource(Res.string.settings_debrid_rule_preferred_channels), stringResource(Res.string.settings_debrid_rule_preferred_channels_desc), selectionCountLabel(preferences.preferredAudioChannels)),
DebridRuleRow(DebridStreamPicker.REQUIRED_AUDIO_CHANNELS, stringResource(Res.string.settings_debrid_rule_required_channels), stringResource(Res.string.settings_debrid_rule_required_channels_desc), selectionCountLabel(preferences.requiredAudioChannels)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_AUDIO_CHANNELS, stringResource(Res.string.settings_debrid_rule_excluded_channels), stringResource(Res.string.settings_debrid_rule_excluded_channels_desc), selectionCountLabel(preferences.excludedAudioChannels)),
DebridRuleRow(DebridStreamPicker.PREFERRED_ENCODES, stringResource(Res.string.settings_debrid_rule_preferred_encodes), stringResource(Res.string.settings_debrid_rule_preferred_encodes_desc), selectionCountLabel(preferences.preferredEncodes)),
DebridRuleRow(DebridStreamPicker.REQUIRED_ENCODES, stringResource(Res.string.settings_debrid_rule_required_encodes), stringResource(Res.string.settings_debrid_rule_required_encodes_desc), selectionCountLabel(preferences.requiredEncodes)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_ENCODES, stringResource(Res.string.settings_debrid_rule_excluded_encodes), stringResource(Res.string.settings_debrid_rule_excluded_encodes_desc), selectionCountLabel(preferences.excludedEncodes)),
DebridRuleRow(DebridStreamPicker.PREFERRED_LANGUAGES, stringResource(Res.string.settings_debrid_rule_preferred_languages), stringResource(Res.string.settings_debrid_rule_preferred_languages_desc), selectionCountLabel(preferences.preferredLanguages)),
DebridRuleRow(DebridStreamPicker.REQUIRED_LANGUAGES, stringResource(Res.string.settings_debrid_rule_required_languages), stringResource(Res.string.settings_debrid_rule_required_languages_desc), selectionCountLabel(preferences.requiredLanguages)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_LANGUAGES, stringResource(Res.string.settings_debrid_rule_excluded_languages), stringResource(Res.string.settings_debrid_rule_excluded_languages_desc), selectionCountLabel(preferences.excludedLanguages)),
DebridRuleRow(DebridStreamPicker.REQUIRED_RELEASE_GROUPS, stringResource(Res.string.settings_debrid_rule_required_release_groups), stringResource(Res.string.settings_debrid_rule_required_release_groups_desc), selectionCountLabel(preferences.requiredReleaseGroups)),
DebridRuleRow(DebridStreamPicker.EXCLUDED_RELEASE_GROUPS, stringResource(Res.string.settings_debrid_rule_excluded_release_groups), stringResource(Res.string.settings_debrid_rule_excluded_release_groups_desc), selectionCountLabel(preferences.excludedReleaseGroups)),
)
@Composable
private fun selectionCountLabel(values: List<*>): String =
if (values.isEmpty()) "Any" else "${values.size} selected"
if (values.isEmpty()) {
stringResource(Res.string.settings_debrid_selection_any)
} else {
stringResource(Res.string.settings_debrid_selection_count, values.size)
}
@Composable
private fun sizeRangeLabel(preferences: DebridStreamPreferences): String =
sizeRangeLabel(preferences.sizeMinGb, preferences.sizeMaxGb)
@Composable
private fun sizeRangeLabel(minGb: Int, maxGb: Int): String =
when {
minGb <= 0 && maxGb <= 0 -> "Any"
minGb <= 0 -> "Up to ${maxGb}GB"
maxGb <= 0 -> "${minGb}GB+"
else -> "${minGb}-${maxGb}GB"
minGb <= 0 && maxGb <= 0 -> stringResource(Res.string.settings_debrid_selection_any)
minGb <= 0 -> stringResource(Res.string.settings_debrid_size_up_to, maxGb)
maxGb <= 0 -> stringResource(Res.string.settings_debrid_size_min, minGb)
else -> stringResource(Res.string.settings_debrid_size_range_value, minGb, maxGb)
}
private fun sortProfileFor(criteria: List<DebridStreamSortCriterion>): DebridSortProfile {
@ -1250,9 +1342,6 @@ private fun sortProfileFor(criteria: List<DebridStreamSortCriterion>): DebridSor
}
}
private fun sortProfileLabel(criteria: List<DebridStreamSortCriterion>): String =
sortProfileLabel(sortProfileFor(criteria))
private fun sortCriteriaForProfile(profile: DebridSortProfile): List<DebridStreamSortCriterion> =
when (profile) {
DebridSortProfile.DEFAULT -> DebridStreamSortCriterion.defaultOrder

View file

@ -57,6 +57,7 @@ import com.nuvio.app.features.player.AvailableLanguageOptions
import com.nuvio.app.features.player.ExternalPlayerApp
import com.nuvio.app.features.player.ExternalPlayerPlatform
import com.nuvio.app.features.player.IosHardwareDecoderMode
import com.nuvio.app.features.player.localizedLabel
import com.nuvio.app.features.player.IosTargetPrimaries
import com.nuvio.app.features.player.IosTargetTransfer
import com.nuvio.app.features.player.PlayerSettingsRepository
@ -72,6 +73,7 @@ import com.nuvio.app.isIos
import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToInt
@ -501,42 +503,42 @@ private fun PlaybackSettingsSection(
if (isIos) {
SettingsSection(
title = "iOS video output",
title = stringResource(Res.string.settings_playback_ios_video_output),
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
SettingsNavigationRow(
title = "Hardware decoder",
description = autoPlayPlayerSettings.iosHardwareDecoderMode.label,
title = stringResource(Res.string.settings_playback_ios_hardware_decoder),
description = autoPlayPlayerSettings.iosHardwareDecoderMode.localizedLabel(),
isTablet = isTablet,
onClick = { showIosHardwareDecoderDialog = true },
)
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = "Extended dynamic range",
description = "Default Metal output mode for new playback sessions.",
title = stringResource(Res.string.settings_playback_ios_extended_dynamic_range),
description = stringResource(Res.string.settings_playback_ios_extended_dynamic_range_desc),
checked = autoPlayPlayerSettings.iosExtendedDynamicRangeEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setIosExtendedDynamicRangeEnabled,
)
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = "Display color hint",
description = "Let mpv target the active display color space by default.",
title = stringResource(Res.string.settings_playback_ios_display_color_hint),
description = stringResource(Res.string.settings_playback_ios_display_color_hint_desc),
checked = autoPlayPlayerSettings.iosTargetColorspaceHintEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setIosTargetColorspaceHintEnabled,
)
SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = "Target primaries",
title = stringResource(Res.string.settings_playback_ios_target_primaries),
description = autoPlayPlayerSettings.iosTargetPrimaries.label,
isTablet = isTablet,
onClick = { showIosTargetPrimariesDialog = true },
)
SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = "Target transfer",
title = stringResource(Res.string.settings_playback_ios_target_transfer),
description = autoPlayPlayerSettings.iosTargetTransfer.label,
isTablet = isTablet,
onClick = { showIosTargetTransferDialog = true },
@ -924,7 +926,7 @@ private fun PlaybackSettingsSection(
if (showIosHardwareDecoderDialog) {
IosEnumSelectionDialog(
title = "Hardware decoder",
title = stringResource(Res.string.settings_playback_ios_hw_decoder_dialog),
options = IosHardwareDecoderMode.entries,
selected = autoPlayPlayerSettings.iosHardwareDecoderMode,
label = { it.label },
@ -938,7 +940,7 @@ private fun PlaybackSettingsSection(
if (showIosTargetPrimariesDialog) {
IosEnumSelectionDialog(
title = "Target primaries",
title = stringResource(Res.string.settings_playback_ios_target_primaries_dialog),
options = IosTargetPrimaries.entries,
selected = autoPlayPlayerSettings.iosTargetPrimaries,
label = { it.label },
@ -952,7 +954,7 @@ private fun PlaybackSettingsSection(
if (showIosTargetTransferDialog) {
IosEnumSelectionDialog(
title = "Target transfer",
title = stringResource(Res.string.settings_playback_ios_target_transfer_dialog),
options = IosTargetTransfer.entries,
selected = autoPlayPlayerSettings.iosTargetTransfer,
label = { it.label },
@ -2379,7 +2381,7 @@ private fun IntroDbApiKeyDialog(
if (isValid) {
onSave(trimmed)
} else {
errorMessage = invalidKeyMessage
errorMessage = getString(Res.string.settings_playback_introdb_invalid_api_key)
}
}
},