aboutsummaryrefslogtreecommitdiff
path: root/src/transcript.rs
diff options
context:
space:
mode:
authorMalte Voos <git@mal.tc>2025-10-01 00:20:10 +0200
committerMalte Voos <git@mal.tc>2025-10-01 00:20:10 +0200
commit338babaad2189f7ff1ee088994c8c20a0646ff4d (patch)
tree29fb2620f748d32a42c1d1eb3346771600a8d75b /src/transcript.rs
downloadlleap-338babaad2189f7ff1ee088994c8c20a0646ff4d.tar.gz
lleap-338babaad2189f7ff1ee088994c8c20a0646ff4d.zip
init
Diffstat (limited to 'src/transcript.rs')
-rw-r--r--src/transcript.rs143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/transcript.rs b/src/transcript.rs
new file mode 100644
index 0000000..2bddb72
--- /dev/null
+++ b/src/transcript.rs
@@ -0,0 +1,143 @@
+use gtk::{ListBox, pango::WrapMode, prelude::*};
+use relm4::prelude::*;
+
+use crate::subtitle_extractor::{StreamIndex, SubtitleCue, TRACKS};
+
+#[derive(Debug)]
+pub enum SubtitleCueOutput {
+ SeekTo(gst::ClockTime),
+}
+
+#[relm4::factory(pub)]
+impl FactoryComponent for SubtitleCue {
+ type Init = Self;
+ type Input = ();
+ type Output = SubtitleCueOutput;
+ type CommandOutput = ();
+ type ParentWidget = gtk::ListBox;
+
+ view! {
+ gtk::Button {
+ inline_css: "padding: 5px; border-radius: 0;",
+ connect_clicked: {
+ let start = self.start;
+ move |_| {
+ sender.output(SubtitleCueOutput::SeekTo(start)).unwrap()
+ }
+ },
+
+ gtk::Label {
+ set_label: &self.text,
+ set_wrap: true,
+ set_wrap_mode: WrapMode::Word,
+ set_xalign: 0.0,
+ add_css_class: "body",
+ }
+ }
+ }
+
+ fn init_model(init: Self::Init, _index: &Self::Index, _sender: FactorySender<Self>) -> Self {
+ init
+ }
+}
+
+pub struct Transcript {
+ active_stream_index: Option<StreamIndex>,
+ active_cues: FactoryVecDeque<SubtitleCue>,
+ pending_scroll: Option<usize>,
+}
+
+#[derive(Debug)]
+pub enum TranscriptMsg {
+ NewCue(StreamIndex, SubtitleCue),
+ SelectTrack(StreamIndex),
+ ScrollToCue(usize),
+}
+
+#[derive(Debug)]
+pub enum TranscriptOutput {
+ SeekTo(gst::ClockTime),
+}
+
+pub struct TranscriptWidgets {
+ viewport: gtk::Viewport,
+}
+
+impl SimpleComponent for Transcript {
+ type Init = ();
+ type Input = TranscriptMsg;
+ type Output = TranscriptOutput;
+ type Widgets = TranscriptWidgets;
+ type Root = gtk::ScrolledWindow;
+
+ fn init(
+ _init: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender<Self>,
+ ) -> ComponentParts<Self> {
+ let listbox = ListBox::builder()
+ .selection_mode(gtk::SelectionMode::None)
+ .build();
+
+ let active_cues =
+ FactoryVecDeque::builder()
+ .launch(listbox)
+ .forward(sender.output_sender(), |output| match output {
+ SubtitleCueOutput::SeekTo(pos) => TranscriptOutput::SeekTo(pos),
+ });
+
+ let model = Self {
+ active_stream_index: None,
+ active_cues,
+ pending_scroll: None,
+ };
+
+ let widgets = TranscriptWidgets {
+ viewport: gtk::Viewport::builder().build(),
+ };
+
+ widgets.viewport.set_child(Some(model.active_cues.widget()));
+ root.set_child(Some(&widgets.viewport));
+
+ ComponentParts { model, widgets }
+ }
+
+ fn init_root() -> Self::Root {
+ gtk::ScrolledWindow::new()
+ }
+
+ fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
+ self.pending_scroll = None;
+
+ match msg {
+ TranscriptMsg::NewCue(stream_index, cue) => {
+ if self.active_stream_index == Some(stream_index) {
+ self.active_cues.guard().push_back(cue);
+ }
+ }
+ TranscriptMsg::SelectTrack(stream_index) => {
+ self.active_stream_index = Some(stream_index);
+
+ // Clear current widgets and populate with selected track's cues
+ self.active_cues.guard().clear();
+ let tracks = TRACKS.read();
+ if let Some(track) = tracks.get(&stream_index) {
+ for cue in &track.cues {
+ self.active_cues.guard().push_back(cue.clone());
+ }
+ }
+ }
+ TranscriptMsg::ScrollToCue(ix) => {
+ self.pending_scroll = Some(ix);
+ }
+ }
+ }
+
+ fn update_view(&self, widgets: &mut Self::Widgets, _sender: ComponentSender<Self>) {
+ if let Some(ix) = self.pending_scroll {
+ if let Some(row) = self.active_cues.widget().row_at_index(ix as i32) {
+ widgets.viewport.scroll_to(&row, None);
+ }
+ }
+ }
+}