diff options
-rw-r--r-- | Cargo.lock | 3 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | build.rs | 5 | ||||
-rw-r--r-- | flake.nix | 7 | ||||
-rw-r--r-- | src/app.rs | 146 | ||||
-rw-r--r-- | src/main.rs | 1 | ||||
-rw-r--r-- | src/player.rs | 13 | ||||
-rw-r--r-- | src/subtitle_extractor.rs | 4 | ||||
-rw-r--r-- | src/subtitle_selection_dialog.rs | 257 | ||||
-rw-r--r-- | src/subtitle_view.rs | 3 | ||||
-rw-r--r-- | src/transcript.rs | 15 | ||||
-rw-r--r-- | src/util/mod.rs | 2 | ||||
-rw-r--r-- | src/util/tracker.rs | 41 |
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 = >k::MenuButton { - set_label: "Select Subtitle Track", - set_popover: Some(>k::PopoverMenu::from_model(Some(&model.subtitle_selection_menu))), - }, pack_start = >k::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()) + } +} |