summary refs log tree commit diff
path: root/src/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.rs')
-rw-r--r--src/app.rs335
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));
 }