diff options
Diffstat (limited to 'src/app.rs')
| -rw-r--r-- | src/app.rs | 335 |
1 files changed, 172 insertions, 163 deletions
diff --git a/src/app.rs b/src/app.rs index 951392e..bdb2ef9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,33 +5,36 @@ use crate::{ icon_names, open_dialog::{OpenDialog, OpenDialogMsg, OpenDialogOutput}, player::{Player, PlayerMsg, PlayerOutput}, - preferences::{Preferences, PreferencesMsg}, - subtitle_extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput}, + preferences_dialog::{PreferencesDialog, PreferencesDialogMsg}, subtitle_selection_dialog::{ SubtitleSelectionDialog, SubtitleSelectionDialogMsg, SubtitleSelectionDialogOutput, + SubtitleSettings, }, subtitle_view::{SubtitleView, SubtitleViewMsg, SubtitleViewOutput}, - tracks::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue}, + subtitles::{ + MetadataCollection, SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack, + extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput}, + state::SubtitleState, + }, transcript::{Transcript, TranscriptMsg, TranscriptOutput}, + translation::{DeeplTranslator, deepl::DeeplTranslatorMsg}, util::Tracker, }; pub struct App { + root: adw::ApplicationWindow, transcript: Controller<Transcript>, player: Controller<Player>, subtitle_view: Controller<SubtitleView>, extractor: WorkerController<SubtitleExtractor>, + deepl_translator: AsyncController<DeeplTranslator>, - preferences: Controller<Preferences>, + preferences: Controller<PreferencesDialog>, open_url_dialog: Controller<OpenDialog>, - subtitle_selection_dialog: Controller<SubtitleSelectionDialog>, + subtitle_selection_dialog: Option<Controller<SubtitleSelectionDialog>>, - primary_stream_ix: Option<StreamIndex>, - primary_cue: Tracker<Option<String>>, - primary_last_cue_ix: Tracker<Option<usize>>, - secondary_cue: Tracker<Option<String>>, - secondary_stream_ix: Option<StreamIndex>, - secondary_last_cue_ix: Tracker<Option<usize>>, + primary_subtitle_state: SubtitleState, + secondary_subtitle_state: SubtitleState, // for auto-pausing autopaused: bool, @@ -40,10 +43,9 @@ pub struct App { #[derive(Debug)] pub enum AppMsg { - NewCue(StreamIndex, SubtitleCue), + AddCue(StreamIndex, SubtitleCue), SubtitleExtractionComplete, - PrimarySubtitleTrackSelected(Option<StreamIndex>), - SecondarySubtitleTrackSelected(Option<StreamIndex>), + ApplySubtitleSettings(SubtitleSettings), PositionUpdate(gst::ClockTime), SetHoveringSubtitleCue(bool), ShowUrlOpenDialog, @@ -51,6 +53,7 @@ pub enum AppMsg { ShowSubtitleSelectionDialog, Play { url: String, + metadata: MetadataCollection, whisper_stream_index: Option<StreamIndex>, }, } @@ -123,52 +126,46 @@ impl SimpleComponent for App { sender.input_sender(), |output| match output { SubtitleExtractorOutput::NewCue(stream_index, cue) => { - AppMsg::NewCue(stream_index, cue) + AppMsg::AddCue(stream_index, cue) } SubtitleExtractorOutput::ExtractionComplete => AppMsg::SubtitleExtractionComplete, }, ); - let preferences = Preferences::builder().launch(root.clone().into()).detach(); + let deepl_translator = DeeplTranslator::builder().launch(()).detach(); + + let preferences = PreferencesDialog::builder() + .launch(root.clone().into()) + .detach(); let open_url_dialog = OpenDialog::builder().launch(root.clone().into()).forward( sender.input_sender(), |output| match output { OpenDialogOutput::Play { url, + metadata, whisper_stream_index, } => AppMsg::Play { url, + metadata, whisper_stream_index, }, }, ); - let subtitle_selection_dialog = SubtitleSelectionDialog::builder() - .launch(root.clone().into()) - .forward(sender.input_sender(), |output| match output { - SubtitleSelectionDialogOutput::PrimaryTrackSelected(ix) => { - AppMsg::PrimarySubtitleTrackSelected(ix) - } - SubtitleSelectionDialogOutput::SecondaryTrackSelected(ix) => { - AppMsg::SecondarySubtitleTrackSelected(ix) - } - }); let model = Self { + root: root.clone(), player, transcript, subtitle_view, extractor, + deepl_translator, preferences, open_url_dialog, - subtitle_selection_dialog, + subtitle_selection_dialog: None, - primary_stream_ix: None, - primary_cue: Tracker::new(None), - primary_last_cue_ix: Tracker::new(None), - secondary_stream_ix: None, - secondary_cue: Tracker::new(None), - secondary_last_cue_ix: Tracker::new(None), + primary_subtitle_state: SubtitleState::default(), + secondary_subtitle_state: SubtitleState::default(), autopaused: false, hovering_primary_cue: false, @@ -179,94 +176,45 @@ impl SimpleComponent for App { ComponentParts { model, widgets } } - fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) { + fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) { match message { - AppMsg::NewCue(stream_index, cue) => { + AppMsg::AddCue(stream_ix, cue) => { + SUBTITLE_TRACKS + .write() + .get_mut(&stream_ix) + .unwrap() + .push_cue(cue.clone()); + self.transcript .sender() - .send(TranscriptMsg::NewCue(stream_index, cue)) + .send(TranscriptMsg::NewCue(stream_ix, cue)) .unwrap(); } AppMsg::SubtitleExtractionComplete => { log::info!("Subtitle extraction complete"); } - AppMsg::PrimarySubtitleTrackSelected(stream_index) => { - self.primary_stream_ix = stream_index; + AppMsg::ApplySubtitleSettings(settings) => { + self.primary_subtitle_state + .set_stream_ix(settings.primary_track_ix); + self.secondary_subtitle_state + .set_stream_ix(settings.secondary_track_ix); self.transcript .sender() - .send(TranscriptMsg::SelectTrack(stream_index)) + .send(TranscriptMsg::SelectTrack(settings.primary_track_ix)) + .unwrap(); + self.deepl_translator + .sender() + .send(DeeplTranslatorMsg::SelectTrack(settings.primary_track_ix)) .unwrap(); - } - AppMsg::SecondarySubtitleTrackSelected(stream_index) => { - self.secondary_stream_ix = stream_index; - } - AppMsg::PositionUpdate(pos) => { - if let Some(stream_ix) = self.primary_stream_ix { - // sometimes we get a few position update messages after - // auto-pausing; this prevents us from immediately un-autopausing - // again - if self.autopaused { - return; - } - - let cue_was_some = self.primary_cue.get().is_some(); - - Self::update_cue( - stream_ix, - pos, - &mut self.primary_cue, - &mut self.primary_last_cue_ix, - ); - - if self.primary_cue.is_dirty() { - // last cue just ended -> auto-pause - if cue_was_some && self.hovering_primary_cue { - self.player.sender().send(PlayerMsg::Pause).unwrap(); - self.autopaused = true; - return; - } - - self.subtitle_view - .sender() - .send(SubtitleViewMsg::SetPrimaryCue( - self.primary_cue.get().clone(), - )) - .unwrap(); - - self.primary_cue.reset(); - } - - if self.primary_last_cue_ix.is_dirty() { - if let Some(ix) = self.primary_last_cue_ix.get() { - self.transcript - .sender() - .send(TranscriptMsg::ScrollToCue(*ix)) - .unwrap(); - } - self.primary_last_cue_ix.reset(); - } - } - if let Some(stream_ix) = self.secondary_stream_ix { - Self::update_cue( - stream_ix, - pos, - &mut self.secondary_cue, - &mut self.secondary_last_cue_ix, - ); - - if !self.autopaused && self.secondary_cue.is_dirty() { - self.subtitle_view - .sender() - .send(SubtitleViewMsg::SetSecondaryCue( - self.secondary_cue.get().clone(), - )) - .unwrap(); - - self.secondary_cue.reset(); - } - } + self.subtitle_view + .sender() + .send(SubtitleViewMsg::ApplySubtitleSettings(settings)) + .unwrap(); + } + AppMsg::PositionUpdate(position) => { + self.update_subtitle_states(position); } AppMsg::SetHoveringSubtitleCue(hovering) => { self.hovering_primary_cue = hovering; @@ -284,17 +232,20 @@ impl SimpleComponent for App { AppMsg::ShowPreferences => { self.preferences .sender() - .send(PreferencesMsg::Show) + .send(PreferencesDialogMsg::Show) .unwrap(); } AppMsg::ShowSubtitleSelectionDialog => { - self.subtitle_selection_dialog - .sender() - .send(SubtitleSelectionDialogMsg::Show) - .unwrap(); + if let Some(ref dialog) = self.subtitle_selection_dialog { + dialog + .sender() + .send(SubtitleSelectionDialogMsg::Show) + .unwrap(); + } } AppMsg::Play { url, + metadata, whisper_stream_index, } => { self.player @@ -308,70 +259,128 @@ impl SimpleComponent for App { whisper_stream_index, }) .unwrap(); + + let subtitle_selection_dialog = SubtitleSelectionDialog::builder() + .launch((self.root.clone().into(), metadata)) + .forward(sender.input_sender(), |output| match output { + SubtitleSelectionDialogOutput::ApplySubtitleSettings(settings) => { + AppMsg::ApplySubtitleSettings(settings) + } + }); + self.subtitle_selection_dialog = Some(subtitle_selection_dialog); } } } } impl App { - fn update_cue( - stream_ix: StreamIndex, - position: gst::ClockTime, - cue: &mut Tracker<Option<String>>, - last_cue_ix: &mut Tracker<Option<usize>>, - ) { - let lock = SUBTITLE_TRACKS.read(); - let track = lock.get(&stream_ix).unwrap(); + fn update_subtitle_states(&mut self, position: gst::ClockTime) { + self.update_primary_subtitle_state(position); + self.update_secondary_subtitle_state(position); + } - // try to find current cue quickly (should usually succeed during playback) - if let Some(ix) = last_cue_ix.get() { - let last_cue = track.cues.get(*ix).unwrap(); - if last_cue.start <= position && position <= last_cue.end { - // still at current cue - return; - } else if let Some(next_cue) = track.cues.get(ix + 1) { - if last_cue.end < position && position < next_cue.start { - // strictly between cues - cue.set(None); - return; - } - if next_cue.start <= position && position <= next_cue.end { - // already in next cue (this happens when one cue immediately - // follows the previous one) - cue.set(Some(next_cue.text.clone())); - last_cue_ix.set(Some(ix + 1)); - return; - } + fn update_primary_subtitle_state(&mut self, position: gst::ClockTime) { + // sometimes we get a few position update messages after + // auto-pausing + if self.autopaused { + return; + } + + update_subtitle_state(&mut self.primary_subtitle_state, position); + + // last cue just ended -> auto-pause + if self.primary_subtitle_state.last_ended_cue_ix.is_dirty() && self.hovering_primary_cue { + self.player.sender().send(PlayerMsg::Pause).unwrap(); + self.autopaused = true; + return; + } + + if self.primary_subtitle_state.is_dirty() { + let cue = self.primary_subtitle_state.active_cue(); + + self.subtitle_view + .sender() + .send(SubtitleViewMsg::SetPrimaryCue(cue)) + .unwrap(); + } + + if self.primary_subtitle_state.last_started_cue_ix.is_dirty() { + if let Some(ix) = *self.primary_subtitle_state.last_started_cue_ix { + self.transcript + .sender() + .send(TranscriptMsg::ScrollToCue(ix)) + .unwrap(); } } - // if we are before the first subtitle, no need to look further - if track.cues.is_empty() || position < track.cues.first().unwrap().start { - cue.set(None); - last_cue_ix.set(None); + self.primary_subtitle_state.reset(); + } + + fn update_secondary_subtitle_state(&mut self, position: gst::ClockTime) { + // sometimes we get a few position update messages after + // auto-pausing + if self.autopaused { return; } - // otherwise, search the whole track (e.g. after seeking) - match track - .cues - .iter() - .enumerate() - .rev() - .find(|(_ix, cue)| cue.start <= position) - { - Some((ix, new_cue)) => { - last_cue_ix.set(Some(ix)); - if position <= new_cue.end { - cue.set(Some(new_cue.text.clone())); - } else { - cue.set(None); - } + update_subtitle_state(&mut self.secondary_subtitle_state, position); + + if self.secondary_subtitle_state.is_dirty() { + let cue = self.secondary_subtitle_state.active_cue(); + + self.subtitle_view + .sender() + .send(SubtitleViewMsg::SetSecondaryCue(cue)) + .unwrap(); + } + + self.secondary_subtitle_state.reset(); + } +} + +fn update_subtitle_state(state: &mut SubtitleState, position: gst::ClockTime) { + if let Some(stream_ix) = state.stream_ix { + let lock = SUBTITLE_TRACKS.read(); + let track = lock.get(&stream_ix).unwrap(); + + update_last_time_ix(&track.start_times, &mut state.last_started_cue_ix, position); + update_last_time_ix(&track.end_times, &mut state.last_ended_cue_ix, position); + } +} + +fn update_last_time_ix( + times: &Vec<gst::ClockTime>, + last_time_ix: &mut Tracker<Option<usize>>, + current_time: gst::ClockTime, +) { + // try to find index quickly (should succeed during normal playback) + if let Some(ix) = last_time_ix.get() { + let t0 = times.get(*ix).unwrap(); + match (times.get(ix + 1), times.get(ix + 2)) { + (None, _) if current_time >= *t0 => { + return; } - None => { - cue.set(None); - last_cue_ix.set(None); + (Some(t1), _) if current_time >= *t0 && current_time < *t1 => { + return; } - }; + (Some(t1), None) if current_time >= *t1 => { + last_time_ix.set(Some(ix + 1)); + return; + } + (Some(t1), Some(t2)) if current_time >= *t1 && current_time < *t2 => { + last_time_ix.set(Some(ix + 1)); + return; + } + _ => {} + } + } + + // if we are before the first timestamp, no need to look further + if times.is_empty() || current_time < *times.first().unwrap() { + last_time_ix.set_if_ne(None); + return; } + + // otherwise, search the whole array (e.g. after seeking) + last_time_ix.set(times.iter().rposition(|time| *time <= current_time)); } |