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); } impl SubtitleTrackInfo { pub fn new( stream_index: StreamIndex, language: Option<&'static str>, title: Option, ) -> 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, #[property(get, set)] language: RefCell, #[property(get, set)] title: RefCell, } #[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, primary_track_ix: Option, secondary_track_ix: Option, } #[derive(Debug)] pub enum SubtitleSelectionDialogMsg { Show, PrimaryTrackChanged(Option), SecondaryTrackChanged(Option), } #[derive(Debug)] pub enum SubtitleSelectionDialogOutput { PrimaryTrackSelected(Option), SecondaryTrackSelected(Option), } #[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::().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::().unwrap(); let item = list_item.item().unwrap(); let track_info = item.downcast_ref::().unwrap(); let vbox = list_item.child().unwrap().downcast::().unwrap(); let language_label = vbox.first_child().unwrap().downcast::().unwrap(); let title_label = vbox.last_child().unwrap().downcast::().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, ) -> ComponentParts { let track_list_model = gio::ListStore::new::(); 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.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 { let ix = combo .selected_item()? .downcast_ref::() .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::() { if track_info.get_stream_index() == stream_ix { return i; } } } } panic!("Stream index {} not found in list model", stream_ix); }