diff --git a/crates/pstr-app/src/app.rs b/crates/pstr-app/src/app.rs index 353d738..a153725 100644 --- a/crates/pstr-app/src/app.rs +++ b/crates/pstr-app/src/app.rs @@ -223,6 +223,13 @@ pub enum Action { PlayAdjacent(Adjacent), /// Start or stop the next episode playing on its own at the end of one. SetAutoplay(bool), + /// The audio and subtitle languages a file is opened with, for titles + /// without a choice of their own. `subtitles` false is "off". + SetLanguages { + audio: Option, + subtitles: bool, + subtitle: Option, + }, /// Play faster or slower, and keep doing so for the next file. SetSpeed(f64), /// Seek past openings and credits without being asked. @@ -1877,6 +1884,17 @@ impl App { prefs.autoplay_next = autoplay; self.engine.set_playback_prefs(prefs, true); } + Action::SetLanguages { + audio, + subtitles, + subtitle, + } => { + let mut prefs = self.engine.playback_prefs(); + prefs.audio_language = audio; + prefs.subtitles = subtitles; + prefs.subtitle_language = subtitle; + self.engine.set_playback_prefs(prefs, true); + } Action::SetAppearance(appearance) => { self.engine.set_appearance(appearance); theme::apply(ctx, appearance); @@ -2218,6 +2236,7 @@ impl eframe::App for App { let prefs = ui::settings::Prefs { autoplay: playback_prefs.autoplay_next, auto_skip: playback_prefs.auto_skip, + playback: &playback_prefs, appearance: self.engine.appearance(), desktop: self.engine.desktop_prefs(), cache_used: (self.page == Page::Settings) diff --git a/crates/pstr-app/src/ui/settings.rs b/crates/pstr-app/src/ui/settings.rs index 98be74b..f17b534 100644 --- a/crates/pstr-app/src/ui/settings.rs +++ b/crates/pstr-app/src/ui/settings.rs @@ -7,6 +7,8 @@ use pstr_core::appearance::{Accent, Appearance, Flavor, Tiles}; use pstr_core::metadata::{MetadataConfig, ProviderId, TitleNames}; +use pstr_core::prefs::PlaybackPrefs; +use pstr_player::LANGUAGES; use crate::app::Action; use crate::desktop_prefs::{CACHE_CHOICES, DesktopPrefs}; @@ -19,9 +21,11 @@ use crate::ui; /// they are read-only, and passing them one by one is what pushed this /// function past the argument count anybody can read. #[derive(Debug, Clone, Copy)] -pub struct Prefs { +pub struct Prefs<'a> { pub autoplay: bool, pub auto_skip: bool, + /// The languages, read from here. + pub playback: &'a PlaybackPrefs, pub appearance: Appearance, pub desktop: DesktopPrefs, /// What the streaming cache holds, once the shares are open. @@ -35,7 +39,7 @@ const COLUMN: f32 = 760.0; pub fn show( ui: &mut egui::Ui, settings: &mut MetadataConfig, - prefs: Prefs, + prefs: Prefs<'_>, api_key: &mut String, actions: &mut Vec, ) { @@ -271,16 +275,18 @@ fn swatch(ui: &mut egui::Ui, appearance: Appearance, accent: Accent) -> egui::Re /// The playback settings that are not per-file. /// -/// Volume, audio language, subtitle language and speed are all set from the -/// player itself, where a viewer can hear the result. -fn playback_form(ui: &mut egui::Ui, prefs: Prefs, actions: &mut Vec) { +/// Volume and speed are set from the player itself, where a viewer can hear +/// the result. The languages are here as well as there: what a first episode +/// opens with is decided before there is a player to choose from. +fn playback_form(ui: &mut egui::Ui, prefs: Prefs<'_>, actions: &mut Vec) { ui::widgets::settings_group( ui, "Playback", Some( - "Volume, audio, subtitles and speed are set from the player, and remembered from there.", + "Volume and speed are set from the player. A language chosen in the player for one show is kept for that show.", ), |ui| { + languages(ui, prefs.playback, actions); let mut autoplay = prefs.autoplay; ui::widgets::settings_row( ui, @@ -322,8 +328,80 @@ fn playback_form(ui: &mut egui::Ui, prefs: Prefs, actions: &mut Vec) { ); } +/// The audio and subtitle languages every file opens with, until a show has a +/// choice of its own. +fn languages(ui: &mut egui::Ui, playback: &PlaybackPrefs, actions: &mut Vec) { + let name = |tag: &Option| match tag { + Some(tag) => pstr_player::language_name(tag), + None => "The file's default".to_owned(), + }; + let audio = playback.audio_language.clone(); + ui::widgets::settings_row( + ui, + "Audio language", + Some("Played when a file has it."), + |ui| { + egui::ComboBox::from_id_salt("audio-language") + .selected_text(name(&audio)) + .show_ui(ui, |ui| { + for choice in + std::iter::once(None).chain(LANGUAGES.map(|tag| Some(tag.to_owned()))) + { + if ui + .selectable_label(audio == choice, name(&choice)) + .clicked() + && audio != choice + { + actions.push(Action::SetLanguages { + audio: choice, + subtitles: playback.subtitles, + subtitle: playback.subtitle_language.clone(), + }); + } + } + }); + }, + ); + // Off, the file's default, or a language: "off" is a decision of its + // own, which no language expresses. + let subtitle = playback + .subtitles + .then(|| playback.subtitle_language.clone()); + let subtitle_name = |choice: &Option>| match choice { + None => "Off".to_owned(), + Some(tag) => name(tag), + }; + ui::widgets::settings_row( + ui, + "Subtitles", + Some("Shown when a file has them in this language."), + |ui| { + egui::ComboBox::from_id_salt("subtitle-language") + .selected_text(subtitle_name(&subtitle)) + .show_ui(ui, |ui| { + let choices = [None, Some(None)] + .into_iter() + .chain(LANGUAGES.map(|tag| Some(Some(tag.to_owned())))); + for choice in choices { + if ui + .selectable_label(subtitle == choice, subtitle_name(&choice)) + .clicked() + && subtitle != choice + { + actions.push(Action::SetLanguages { + audio: playback.audio_language.clone(), + subtitles: choice.is_some(), + subtitle: choice.flatten(), + }); + } + } + }); + }, + ); +} + /// The streaming cache: how big it may get, and a way to empty it. -fn storage_form(ui: &mut egui::Ui, prefs: Prefs, actions: &mut Vec) { +fn storage_form(ui: &mut egui::Ui, prefs: Prefs<'_>, actions: &mut Vec) { ui::widgets::settings_group( ui, "Storage", diff --git a/crates/pstr-player/src/lib.rs b/crates/pstr-player/src/lib.rs index 24e112c..1005b5d 100644 --- a/crates/pstr-player/src/lib.rs +++ b/crates/pstr-player/src/lib.rs @@ -52,4 +52,4 @@ pub use player::{ }; pub use registry::{PROTOCOL, StreamHandle, StreamRegistry}; pub use render::VideoRenderer; -pub use tracks::{Track, TrackKind, language_name, pick_track}; +pub use tracks::{LANGUAGES, Track, TrackKind, language_name, pick_track}; diff --git a/crates/pstr-player/src/tracks.rs b/crates/pstr-player/src/tracks.rs index 911e004..7f2dcf0 100644 --- a/crates/pstr-player/src/tracks.rs +++ b/crates/pstr-player/src/tracks.rs @@ -190,6 +190,16 @@ fn flag(mpv: &Mpv, index: i64, field: &str) -> bool { .unwrap_or(false) } +/// The languages offered by name where a preference is chosen ahead of any +/// file, as the three-letter tag a Matroska file carries. The same list +/// [`language_name`] knows, and the phones' `LANGUAGES`; mpv treats the two- +/// and three-letter forms as one. +pub const LANGUAGES: [&str; 29] = [ + "jpn", "eng", "ger", "fre", "spa", "ita", "por", "dut", "rus", "pol", "swe", "dan", "nor", + "fin", "cze", "hun", "tur", "ara", "heb", "hin", "kor", "chi", "tha", "vie", "ukr", "rum", + "gre", "ind", "may", +]; + /// A readable name for a language tag, falling back to the tag itself. /// /// Deliberately a short list rather than a full ISO 639 table: it covers what