aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock3
-rw-r--r--Cargo.toml2
-rw-r--r--build.rs5
-rw-r--r--flake.nix7
-rw-r--r--src/app.rs146
-rw-r--r--src/main.rs1
-rw-r--r--src/player.rs13
-rw-r--r--src/subtitle_extractor.rs4
-rw-r--r--src/subtitle_selection_dialog.rs257
-rw-r--r--src/subtitle_view.rs3
-rw-r--r--src/transcript.rs15
-rw-r--r--src/util/mod.rs2
-rw-r--r--src/util/tracker.rs41
13 files changed, 382 insertions, 117 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f5608ef..7ca1eff 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -888,8 +888,7 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "isolang"
version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe50d48c77760c55188549098b9a7f6e37ae980c586a24693d6b01c3b2010c3c"
+source = "git+https://github.com/humenda/isolang-rs#2e184a9a9d29d82561aedf2a3f5b91b9b78c7d1f"
dependencies = [
"phf",
]
diff --git a/Cargo.toml b/Cargo.toml
index 9d2e084..ca38001 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,7 +18,7 @@ env_logger = "0.11"
log = "0.4"
tracker = "0.2.2"
unicode-segmentation = "1.12.0"
-isolang = "2.4.0"
+isolang = { git = "https://github.com/humenda/isolang-rs" }
# TODO remove
diff --git a/build.rs b/build.rs
index 9e7d61e..875e8c6 100644
--- a/build.rs
+++ b/build.rs
@@ -3,29 +3,24 @@ use std::path::Path;
use std::process::Command;
fn main() {
- // Tell cargo to rerun this build script if the schema file changes
println!("cargo:rerun-if-changed=data/tc.mal.lleap.gschema.xml");
let out_dir = env::var("OUT_DIR").unwrap();
let schema_dir = Path::new(&out_dir).join("glib-2.0").join("schemas");
- // Create the schema directory
std::fs::create_dir_all(&schema_dir).unwrap();
- // Copy the schema file to the output directory
std::fs::copy(
"data/tc.mal.lleap.gschema.xml",
schema_dir.join("tc.mal.lleap.gschema.xml"),
)
.unwrap();
- // Compile the schema using glib-compile-schemas
Command::new("glib-compile-schemas")
.arg(&schema_dir)
.output()
.unwrap();
- // Set environment variable for the schema directory
println!(
"cargo:rustc-env=GSETTINGS_SCHEMA_DIR={}",
schema_dir.display()
diff --git a/flake.nix b/flake.nix
index 4d73bfc..a5e7874 100644
--- a/flake.nix
+++ b/flake.nix
@@ -47,6 +47,8 @@
nativeBuildInputs = with pkgs; [
pkg-config
rustPlatform.bindgenHook
+ wrapGAppsHook4
+ glib
];
buildInputs = with pkgs; [
@@ -62,6 +64,11 @@
gst_all_1.gst-vaapi
ffmpeg_7-full.dev
];
+
+ postInstall = ''
+ install -D -m444 -t $out/share/glib-2.0/schemas data/*.gschema.xml
+ glib-compile-schemas $out/share/glib-2.0/schemas
+ '';
};
devShell = pkgs.mkShell {
diff --git a/src/app.rs b/src/app.rs
index 10c20e6..18f03e8 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,6 +1,4 @@
use adw::prelude::*;
-use gst::glib::clone;
-use gtk::gio::{Menu, MenuItem, SimpleAction, SimpleActionGroup};
use relm4::{WorkerController, prelude::*};
use crate::{
@@ -9,13 +7,14 @@ use crate::{
subtitle_extractor::{
StreamIndex, SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput, TRACKS,
},
+ subtitle_selection_dialog::{
+ SubtitleSelectionDialog, SubtitleSelectionDialogMsg, SubtitleSelectionDialogOutput,
+ },
subtitle_view::{SubtitleView, SubtitleViewMsg, SubtitleViewOutput},
transcript::{Transcript, TranscriptMsg, TranscriptOutput},
util::OptionTracker,
};
-const TRACK_SELECTION_ACTION_GROUP_NAME: &str = "subtitle_track_selection";
-
pub struct App {
url: String,
transcript: Controller<Transcript>,
@@ -23,9 +22,7 @@ pub struct App {
subtitle_view: Controller<SubtitleView>,
extractor: WorkerController<SubtitleExtractor>,
preferences: Controller<Preferences>,
-
- subtitle_selection_menu: Menu,
- subtitle_selection_action_group: SimpleActionGroup,
+ subtitle_selection_dialog: Controller<SubtitleSelectionDialog>,
primary_stream_ix: Option<StreamIndex>,
primary_last_cue_ix: OptionTracker<usize>,
@@ -42,11 +39,13 @@ pub struct App {
pub enum AppMsg {
NewOrUpdatedTrackMetadata(StreamIndex),
NewCue(StreamIndex, crate::subtitle_extractor::SubtitleCue),
- ExtractionComplete,
- TrackSelected(StreamIndex),
+ SubtitleExtractionComplete,
+ PrimarySubtitleTrackSelected(Option<StreamIndex>),
+ SecondarySubtitleTrackSelected(Option<StreamIndex>),
PositionUpdate(gst::ClockTime),
SetHoveringSubtitleCue(bool),
ShowPreferences,
+ ShowSubtitleSelectionDialog,
}
#[relm4::component(pub)]
@@ -65,14 +64,9 @@ impl SimpleComponent for App {
#[name(toolbar_view)]
adw::ToolbarView {
add_top_bar = &adw::HeaderBar {
- pack_start = &gtk::MenuButton {
- set_label: "Select Subtitle Track",
- set_popover: Some(&gtk::PopoverMenu::from_model(Some(&model.subtitle_selection_menu))),
- },
pack_start = &gtk::Button {
- set_label: "Preferences",
+ set_icon_name: "settings-symbolic",
connect_clicked => AppMsg::ShowPreferences,
- add_css_class: "flat",
}
},
@@ -96,14 +90,6 @@ impl SimpleComponent for App {
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
- let subtitle_selection_menu = Menu::new();
- let subtitle_selection_action_group = SimpleActionGroup::new();
- root.insert_action_group(
- TRACK_SELECTION_ACTION_GROUP_NAME,
- Some(&subtitle_selection_action_group),
- );
- Self::add_dummy_menu_item(&subtitle_selection_action_group, &subtitle_selection_menu);
-
let subtitle_view = SubtitleView::builder().launch(()).forward(
sender.input_sender(),
|output| match output {
@@ -114,6 +100,7 @@ impl SimpleComponent for App {
.launch(())
.forward(sender.input_sender(), |output| match output {
PlayerOutput::PositionUpdate(pos) => AppMsg::PositionUpdate(pos),
+ PlayerOutput::SubtitleSelectionButtonPressed => AppMsg::ShowSubtitleSelectionDialog,
});
let transcript =
Transcript::builder()
@@ -131,11 +118,21 @@ impl SimpleComponent for App {
SubtitleExtractorOutput::NewCue(stream_index, cue) => {
AppMsg::NewCue(stream_index, cue)
}
- SubtitleExtractorOutput::ExtractionComplete => AppMsg::ExtractionComplete,
+ SubtitleExtractorOutput::ExtractionComplete => AppMsg::SubtitleExtractionComplete,
},
);
let preferences = Preferences::builder().launch(root.clone().into()).detach();
+ 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 {
url: url.clone(), // TODO remove clone
@@ -144,8 +141,7 @@ impl SimpleComponent for App {
subtitle_view,
extractor,
preferences,
- subtitle_selection_menu,
- subtitle_selection_action_group,
+ subtitle_selection_dialog,
primary_stream_ix: None,
primary_last_cue_ix: OptionTracker::new(None),
@@ -173,31 +169,32 @@ impl SimpleComponent for App {
ComponentParts { model, widgets }
}
- fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
+ fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
self.primary_last_cue_ix.reset();
self.secondary_last_cue_ix.reset();
match msg {
- AppMsg::NewOrUpdatedTrackMetadata(_stream_index) => {
- self.update_subtitle_selection_menu(&sender);
- }
+ AppMsg::NewOrUpdatedTrackMetadata(_stream_index) => {}
AppMsg::NewCue(stream_index, cue) => {
self.transcript
.sender()
.send(TranscriptMsg::NewCue(stream_index, cue))
.unwrap();
}
- AppMsg::ExtractionComplete => {
- println!("Subtitle extraction complete");
+ AppMsg::SubtitleExtractionComplete => {
+ log::info!("Subtitle extraction complete");
}
- AppMsg::TrackSelected(stream_index) => {
- self.primary_stream_ix = Some(stream_index);
+ AppMsg::PrimarySubtitleTrackSelected(stream_index) => {
+ self.primary_stream_ix = stream_index;
self.transcript
.sender()
.send(TranscriptMsg::SelectTrack(stream_index))
.unwrap();
}
+ AppMsg::SecondarySubtitleTrackSelected(stream_index) => {
+ self.secondary_stream_ix = stream_index;
+ }
AppMsg::PositionUpdate(pos) => {
if let Some(stream_ix) = self.primary_stream_ix {
let cue =
@@ -239,14 +236,18 @@ impl SimpleComponent for App {
}
}
if let Some(stream_ix) = self.secondary_stream_ix {
- self.subtitle_view
- .sender()
- .send(SubtitleViewMsg::SetPrimaryCue(Self::get_cue_and_update_ix(
- stream_ix,
- pos,
- &mut self.primary_last_cue_ix,
- )))
- .unwrap();
+ if !self.autopaused {
+ self.subtitle_view
+ .sender()
+ .send(SubtitleViewMsg::SetSecondaryCue(
+ Self::get_cue_and_update_ix(
+ stream_ix,
+ pos,
+ &mut self.secondary_last_cue_ix,
+ ),
+ ))
+ .unwrap();
+ }
}
}
AppMsg::SetHoveringSubtitleCue(hovering) => {
@@ -262,66 +263,17 @@ impl SimpleComponent for App {
.send(PreferencesMsg::Show)
.unwrap();
}
+ AppMsg::ShowSubtitleSelectionDialog => {
+ self.subtitle_selection_dialog
+ .sender()
+ .send(SubtitleSelectionDialogMsg::Show)
+ .unwrap();
+ }
}
}
}
impl App {
- fn update_subtitle_selection_menu(&mut self, sender: &ComponentSender<Self>) {
- self.subtitle_selection_menu.remove_all();
-
- for action_name in self.subtitle_selection_action_group.list_actions() {
- self.subtitle_selection_action_group
- .remove_action(&action_name);
- }
-
- let tracks = TRACKS.read();
- if tracks.is_empty() {
- Self::add_dummy_menu_item(
- &self.subtitle_selection_action_group,
- &self.subtitle_selection_menu,
- );
- } else {
- for (stream_index, track) in tracks.iter() {
- let unknown_string = "<unknown>".to_string();
- let language = track.language_code.as_ref().unwrap_or(&unknown_string);
- let label = format!("{} (Stream {})", language, stream_index);
-
- let action_name = format!("select_{}", stream_index);
- let action = SimpleAction::new(&action_name, None);
-
- action.connect_activate(clone!(
- #[strong]
- sender,
- #[strong]
- stream_index,
- move |_, _| {
- let _ = sender.input(AppMsg::TrackSelected(stream_index));
- }
- ));
-
- self.subtitle_selection_action_group.add_action(&action);
-
- // Create menu item
- let action_target =
- format!("{}.{}", TRACK_SELECTION_ACTION_GROUP_NAME, action_name);
- let item = MenuItem::new(Some(&label), Some(&action_target));
- self.subtitle_selection_menu.append_item(&item);
- }
- }
- }
-
- // Add disabled "No tracks available" item
- fn add_dummy_menu_item(action_group: &SimpleActionGroup, menu: &Menu) {
- let disabled_action = SimpleAction::new("no_tracks", None);
- disabled_action.set_enabled(false);
- action_group.add_action(&disabled_action);
-
- let action_target = format!("{}.no_tracks", TRACK_SELECTION_ACTION_GROUP_NAME);
- let item = MenuItem::new(Some("No tracks available"), Some(&action_target));
- menu.append_item(&item);
- }
-
fn get_cue_and_update_ix(
stream_ix: StreamIndex,
position: gst::ClockTime,
diff --git a/src/main.rs b/src/main.rs
index d902eaa..6ce1ca9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,6 +3,7 @@ mod cue_view;
mod player;
mod preferences;
mod subtitle_extractor;
+mod subtitle_selection_dialog;
mod subtitle_view;
mod transcript;
mod util;
diff --git a/src/player.rs b/src/player.rs
index c784a04..2e234b6 100644
--- a/src/player.rs
+++ b/src/player.rs
@@ -37,6 +37,7 @@ pub enum PlayerMsg {
#[derive(Debug)]
pub enum PlayerOutput {
PositionUpdate(gst::ClockTime),
+ SubtitleSelectionButtonPressed,
}
fn format_time(time: gst::ClockTime) -> String {
@@ -74,12 +75,10 @@ impl SimpleComponent for Player {
set_margin_all: 10,
// Play/Pause button
- #[name = "play_pause_btn"]
gtk::Button {
#[watch]
- set_icon_name: if model.is_playing { "media-playback-pause" } else { "media-playback-start" },
+ set_icon_name: if model.is_playing { "media-playback-pause-symbolic" } else { "media-playback-start-symbolic" },
connect_clicked => PlayerMsg::PlayPause,
- add_css_class: "circular",
},
// Current time label
@@ -116,6 +115,14 @@ impl SimpleComponent for Player {
set_text: &format_time(model.duration),
set_width_chars: 8,
},
+
+ // Subtitle selection button
+ gtk::Button {
+ set_icon_name: "media-view-subtitles-symbolic",
+ connect_clicked[sender] => move |_| {
+ sender.output(PlayerOutput::SubtitleSelectionButtonPressed).unwrap();
+ },
+ },
}
}
}
diff --git a/src/subtitle_extractor.rs b/src/subtitle_extractor.rs
index 53655a0..b628d73 100644
--- a/src/subtitle_extractor.rs
+++ b/src/subtitle_extractor.rs
@@ -17,7 +17,7 @@ pub struct SubtitleCue {
#[derive(Debug, Clone)]
pub struct SubtitleTrack {
- pub language_code: Option<String>,
+ pub language: Option<isolang::Language>,
pub title: Option<String>,
pub cues: Vec<SubtitleCue>,
}
@@ -87,7 +87,7 @@ impl SubtitleExtractor {
let title = stream.metadata().get("title").map(|s| s.to_string());
let track = SubtitleTrack {
- language_code,
+ language: language_code.and_then(|code| isolang::Language::from_639_2b(&code)),
title,
cues: Vec::new(),
};
diff --git a/src/subtitle_selection_dialog.rs b/src/subtitle_selection_dialog.rs
new file mode 100644
index 0000000..0c7f1cd
--- /dev/null
+++ b/src/subtitle_selection_dialog.rs
@@ -0,0 +1,257 @@
+use adw::prelude::*;
+use gtk::{gio, glib};
+use relm4::prelude::*;
+
+use crate::subtitle_extractor::{StreamIndex, TRACKS};
+use crate::util::Tracker;
+
+// Custom GObject wrapper for subtitle track information
+glib::wrapper! {
+ pub struct SubtitleTrackInfo(ObjectSubclass<imp::SubtitleTrackInfo>);
+}
+
+impl SubtitleTrackInfo {
+ pub fn new(
+ stream_index: StreamIndex,
+ language: Option<&'static str>,
+ title: Option<String>,
+ ) -> Self {
+ glib::Object::builder()
+ .property("stream-index", stream_index as i64)
+ .property("language", language.unwrap_or_default())
+ .property("title", title.unwrap_or_default())
+ .build()
+ }
+
+ pub fn get_stream_index(&self) -> StreamIndex {
+ let index: i64 = self.property("stream-index");
+ index as usize
+ }
+}
+
+mod imp {
+ use gtk::{glib, prelude::*, subclass::prelude::*};
+ use std::cell::RefCell;
+
+ #[derive(Default, glib::Properties)]
+ #[properties(wrapper_type = super::SubtitleTrackInfo)]
+ pub struct SubtitleTrackInfo {
+ #[property(get, set)]
+ stream_index: RefCell<i64>,
+ #[property(get, set)]
+ language: RefCell<String>,
+ #[property(get, set)]
+ title: RefCell<String>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for SubtitleTrackInfo {
+ const NAME: &'static str = "SubtitleTrackInfo";
+ type Type = super::SubtitleTrackInfo;
+ }
+
+ #[glib::derived_properties]
+ impl ObjectImpl for SubtitleTrackInfo {}
+}
+
+pub struct SubtitleSelectionDialog {
+ parent_window: adw::ApplicationWindow,
+ dialog: adw::PreferencesDialog,
+ track_list_model: Tracker<gio::ListStore>,
+ primary_track_ix: Option<StreamIndex>,
+ secondary_track_ix: Option<StreamIndex>,
+}
+
+#[derive(Debug)]
+pub enum SubtitleSelectionDialogMsg {
+ Show,
+ PrimaryTrackChanged(Option<StreamIndex>),
+ SecondaryTrackChanged(Option<StreamIndex>),
+}
+
+#[derive(Debug)]
+pub enum SubtitleSelectionDialogOutput {
+ PrimaryTrackSelected(Option<StreamIndex>),
+ SecondaryTrackSelected(Option<StreamIndex>),
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for SubtitleSelectionDialog {
+ type Init = adw::ApplicationWindow;
+ type Input = SubtitleSelectionDialogMsg;
+ type Output = SubtitleSelectionDialogOutput;
+
+ view! {
+ #[root]
+ adw::PreferencesDialog {
+ set_title: "Subtitle Settings",
+ add: &page,
+ },
+
+ #[name(page)]
+ adw::PreferencesPage {
+ adw::PreferencesGroup {
+ #[name(primary_combo)]
+ adw::ComboRow {
+ set_title: "Primary Subtitle Track",
+ set_subtitle: "Main subtitle track for learning",
+ set_factory: Some(&track_factory),
+ #[track(model.track_list_model.is_dirty())]
+ set_model: Some(model.track_list_model.get()),
+ #[track(model.track_list_model.is_dirty())]
+ set_selected: model.primary_track_ix.map_or(gtk::INVALID_LIST_POSITION, |ix| get_list_ix_from_stream_ix(model.track_list_model.get(), ix)),
+ connect_selected_notify[sender] => move |combo| {
+ let stream_index = get_stream_ix_from_combo(combo);
+ sender.input(SubtitleSelectionDialogMsg::PrimaryTrackChanged(stream_index));
+ },
+ },
+
+ #[name(secondary_combo)]
+ adw::ComboRow {
+ set_title: "Secondary Subtitle Track",
+ set_subtitle: "Optional second track for comparison",
+ set_factory: Some(&track_factory),
+ #[track(model.track_list_model.is_dirty())]
+ set_model: Some(model.track_list_model.get()),
+ #[track(model.track_list_model.is_dirty())]
+ set_selected: model.secondary_track_ix.map_or(gtk::INVALID_LIST_POSITION, |ix| get_list_ix_from_stream_ix(model.track_list_model.get(), ix)),
+ connect_selected_notify[sender] => move |combo| {
+ let stream_index = get_stream_ix_from_combo(combo);
+ sender.input(SubtitleSelectionDialogMsg::SecondaryTrackChanged(stream_index));
+ },
+ },
+ }
+ },
+
+ #[name(track_factory)]
+ gtk::SignalListItemFactory {
+ connect_setup => move |_, list_item| {
+ let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
+ let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
+
+ let language_label = gtk::Label::new(None);
+ language_label.set_halign(gtk::Align::Start);
+ language_label.set_ellipsize(gtk::pango::EllipsizeMode::End);
+
+ let title_label = gtk::Label::new(None);
+ title_label.set_halign(gtk::Align::Start);
+ title_label.set_ellipsize(gtk::pango::EllipsizeMode::End);
+ title_label.add_css_class("subtitle");
+
+ vbox.append(&language_label);
+ vbox.append(&title_label);
+ list_item.set_child(Some(&vbox));
+ },
+ connect_bind => move |_, list_item| {
+ let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
+ let item = list_item.item().unwrap();
+ let track_info = item.downcast_ref::<SubtitleTrackInfo>().unwrap();
+ let vbox = list_item.child().unwrap().downcast::<gtk::Box>().unwrap();
+ let language_label = vbox.first_child().unwrap().downcast::<gtk::Label>().unwrap();
+ let title_label = vbox.last_child().unwrap().downcast::<gtk::Label>().unwrap();
+
+ let language = track_info.language();
+ let title = track_info.title();
+
+ let language_text = if !language.is_empty() {
+ &language
+ } else {
+ "Unknown Language"
+ };
+
+ language_label.set_text(&language_text);
+ title_label.set_text(&title);
+ title_label.set_visible(!title.is_empty());
+ },
+ },
+ }
+
+ fn init(
+ parent_window: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender<Self>,
+ ) -> ComponentParts<Self> {
+ let track_list_model = gio::ListStore::new::<SubtitleTrackInfo>();
+
+ let model = Self {
+ parent_window,
+ dialog: root.clone(),
+ track_list_model: Tracker::new(track_list_model),
+ primary_track_ix: None,
+ secondary_track_ix: None,
+ };
+
+ let widgets = view_output!();
+
+ ComponentParts { model, widgets }
+ }
+
+ fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
+ self.track_list_model.reset();
+
+ match msg {
+ SubtitleSelectionDialogMsg::Show => {
+ self.update_combo_models();
+ self.dialog.present(Some(&self.parent_window));
+ }
+ SubtitleSelectionDialogMsg::PrimaryTrackChanged(stream_index) => {
+ self.primary_track_ix = stream_index;
+ sender
+ .output(SubtitleSelectionDialogOutput::PrimaryTrackSelected(
+ stream_index,
+ ))
+ .unwrap();
+ }
+ SubtitleSelectionDialogMsg::SecondaryTrackChanged(stream_index) => {
+ self.secondary_track_ix = stream_index;
+ sender
+ .output(SubtitleSelectionDialogOutput::SecondaryTrackSelected(
+ stream_index,
+ ))
+ .unwrap();
+ }
+ }
+ }
+}
+
+impl SubtitleSelectionDialog {
+ fn update_combo_models(&mut self) {
+ let tracks = TRACKS.read();
+
+ // Clear previous entries
+ self.track_list_model.get_mut().remove_all();
+
+ // Add all available tracks
+ for (&stream_index, track) in tracks.iter() {
+ let track_info = SubtitleTrackInfo::new(
+ stream_index,
+ track.language.map(|lang| lang.to_name()),
+ track.title.clone(),
+ );
+ self.track_list_model.get_mut().append(&track_info);
+ }
+ }
+}
+
+fn get_stream_ix_from_combo(combo: &adw::ComboRow) -> Option<StreamIndex> {
+ let ix = combo
+ .selected_item()?
+ .downcast_ref::<SubtitleTrackInfo>()
+ .unwrap()
+ .get_stream_index();
+
+ Some(ix)
+}
+
+fn get_list_ix_from_stream_ix(list_model: &gio::ListStore, stream_ix: StreamIndex) -> u32 {
+ for i in 0..list_model.n_items() {
+ if let Some(item) = list_model.item(i) {
+ if let Some(track_info) = item.downcast_ref::<SubtitleTrackInfo>() {
+ if track_info.get_stream_index() == stream_ix {
+ return i;
+ }
+ }
+ }
+ }
+ panic!("Stream index {} not found in list model", stream_ix);
+}
diff --git a/src/subtitle_view.rs b/src/subtitle_view.rs
index 30c089c..dc48561 100644
--- a/src/subtitle_view.rs
+++ b/src/subtitle_view.rs
@@ -1,5 +1,6 @@
use crate::cue_view::{CueView, CueViewMsg, CueViewOutput};
use crate::util::OptionTracker;
+use gtk::glib;
use gtk::prelude::*;
use relm4::prelude::*;
@@ -44,7 +45,7 @@ impl SimpleComponent for SubtitleView {
gtk::Label {
#[track = "model.secondary_cue.is_dirty()"]
- set_text: model.secondary_cue.get().as_ref().map(|val| val.as_str()).unwrap_or("TODO placeholder"),
+ set_text: model.secondary_cue.get().as_ref().map(|val| val.as_str()).unwrap_or(""),
set_justify: gtk::Justification::Center,
},
diff --git a/src/transcript.rs b/src/transcript.rs
index 2bddb72..eb3459d 100644
--- a/src/transcript.rs
+++ b/src/transcript.rs
@@ -50,7 +50,7 @@ pub struct Transcript {
#[derive(Debug)]
pub enum TranscriptMsg {
NewCue(StreamIndex, SubtitleCue),
- SelectTrack(StreamIndex),
+ SelectTrack(Option<StreamIndex>),
ScrollToCue(usize),
}
@@ -116,14 +116,17 @@ impl SimpleComponent for Transcript {
}
}
TranscriptMsg::SelectTrack(stream_index) => {
- self.active_stream_index = Some(stream_index);
+ self.active_stream_index = 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());
+
+ if let Some(stream_ix) = stream_index {
+ let tracks = TRACKS.read();
+ if let Some(track) = tracks.get(&stream_ix) {
+ for cue in &track.cues {
+ self.active_cues.guard().push_back(cue.clone());
+ }
}
}
}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 5b0c6ac..600d572 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,3 +1,5 @@
mod option_tracker;
+mod tracker;
pub use option_tracker::OptionTracker;
+pub use tracker::Tracker;
diff --git a/src/util/tracker.rs b/src/util/tracker.rs
new file mode 100644
index 0000000..66c30a9
--- /dev/null
+++ b/src/util/tracker.rs
@@ -0,0 +1,41 @@
+pub struct Tracker<T> {
+ inner: T,
+ dirty: bool,
+}
+
+/// Tracks changes to an inner value T. Any change using `set` will cause the
+/// tracker to be marked as dirty.
+impl<T> Tracker<T> {
+ pub fn new(inner: T) -> Self {
+ Self { inner, dirty: true }
+ }
+
+ pub fn get(&self) -> &T {
+ &self.inner
+ }
+
+ pub fn get_mut(&mut self) -> &mut T {
+ self.dirty = true;
+ &mut self.inner
+ }
+
+ pub fn set(&mut self, value: T) {
+ self.dirty = true;
+ self.inner = value;
+ }
+
+ pub fn is_dirty(&self) -> bool {
+ self.dirty
+ }
+
+ /// Marks the tracker as clean.
+ pub fn reset(&mut self) {
+ self.dirty = false;
+ }
+}
+
+impl<T: Default> Default for Tracker<T> {
+ fn default() -> Self {
+ Self::new(T::default())
+ }
+}