use std::collections::BTreeMap; use adw::prelude::*; use gtk::gio; use gtk::glib::clone; use relm4::prelude::*; use crate::track_selector::{ TrackInfo, TrackSelector, TrackSelectorInit, TrackSelectorMsg, TrackSelectorOutput, }; use crate::tracks::{StreamIndex, TrackMetadata}; use crate::util::Tracker; pub struct OpenDialog { parent_window: adw::ApplicationWindow, dialog: adw::PreferencesDialog, toast_overlay: Option, navigation_view: Option, whisper_track_selector: Controller, url: Tracker, do_whisper_extraction: bool, whisper_stream_index: Option, metadata_command_running: bool, } #[derive(Debug)] pub enum OpenDialogMsg { Show, Next, Cancel, SelectFile, FileSelected(gio::File), UrlChanged(String), SetDoWhisperExtraction(bool), WhisperTrackSelected(Option), Play, } #[derive(Debug)] pub enum OpenDialogOutput { Play { url: String, whisper_stream_index: Option, }, } #[relm4::component(pub)] impl Component for OpenDialog { type Init = adw::ApplicationWindow; type Input = OpenDialogMsg; type Output = OpenDialogOutput; type CommandOutput = Result, ffmpeg::Error>; view! { #[root] adw::PreferencesDialog { set_title: "Open URL", #[wrap(Some)] #[name(toast_overlay)] set_child = &adw::ToastOverlay { #[wrap(Some)] #[name(navigation_view)] set_child = &adw::NavigationView { add = &adw::NavigationPage { set_title: "Open File or Stream", #[wrap(Some)] set_child = &adw::ToolbarView { add_top_bar = &adw::HeaderBar { set_show_end_title_buttons: false, pack_start = >k::Button { set_label: "Cancel", connect_clicked => OpenDialogMsg::Cancel, }, pack_end = >k::Button { set_label: "Next", #[watch] set_sensitive: !(model.url.get().is_empty() || model.metadata_command_running), connect_clicked => OpenDialogMsg::Next, add_css_class: "suggested-action", }, pack_end = &adw::Spinner { #[watch] set_visible: model.metadata_command_running, }, }, #[wrap(Some)] set_content = &adw::PreferencesPage { adw::PreferencesGroup { set_title: "Open a file from your computer", adw::ButtonRow { set_title: "Select File", connect_activated => OpenDialogMsg::SelectFile, } }, adw::PreferencesGroup { set_title: "Or, enter a stream URL", set_description: Some("Currently, only file:// and http(s):// URLs are officially supported, although other protocols may work as well."), adw::EntryRow { set_title: "URL", #[track(model.url.is_dirty())] set_text: model.url.get(), connect_changed[sender] => move |entry| { sender.input(OpenDialogMsg::UrlChanged(entry.text().to_string())); }, } } } } }, add = &adw::NavigationPage { set_tag = Some("playback_options"), set_title: "Playback Options", #[wrap(Some)] set_child = &adw::ToolbarView { add_top_bar = &adw::HeaderBar { set_show_end_title_buttons: false, pack_end = >k::Button { connect_clicked => OpenDialogMsg::Play, add_css_class: "suggested-action", gtk::Label { set_text: "Play", } }, }, #[wrap(Some)] set_content = &adw::PreferencesPage { adw::PreferencesGroup { adw::ExpanderRow { set_title: "Generate subtitles from audio", set_subtitle: "See also \"Whisper settings\" in Preferences", set_show_enable_switch: true, #[watch] set_enable_expansion: model.do_whisper_extraction, connect_enable_expansion_notify[sender] => move |expander_row| { sender.input(OpenDialogMsg::SetDoWhisperExtraction(expander_row.enables_expansion())) }, add_row: model.whisper_track_selector.widget(), }, }, } } } } } } } fn init( parent_window: Self::Init, root: Self::Root, sender: ComponentSender, ) -> ComponentParts { let whisper_track_selector = TrackSelector::builder() .launch(TrackSelectorInit { title: "Audio track", subtitle: None, }) .forward(sender.input_sender(), |output| match output { TrackSelectorOutput::Changed(ix) => OpenDialogMsg::WhisperTrackSelected(ix), }); let mut model = Self { parent_window, dialog: root.clone(), toast_overlay: None, navigation_view: None, whisper_track_selector, url: Tracker::new(String::new()), do_whisper_extraction: false, whisper_stream_index: None, metadata_command_running: false, }; let widgets = view_output!(); model.toast_overlay = Some(widgets.toast_overlay.clone()); model.navigation_view = Some(widgets.navigation_view.clone()); ComponentParts { model, widgets } } fn update(&mut self, message: Self::Input, sender: ComponentSender, _root: &Self::Root) { match message { OpenDialogMsg::Show => { self.reset(); self.dialog.present(Some(&self.parent_window)); } OpenDialogMsg::UrlChanged(url) => self.url.set_clean(url), OpenDialogMsg::Next => self.fetch_metadata(sender), OpenDialogMsg::Cancel => { self.dialog.close(); } OpenDialogMsg::SelectFile => { let dialog = gtk::FileDialog::new(); dialog.open( Some(&self.parent_window), None as Option<&gio::Cancellable>, clone!( #[strong] sender, move |res| { if let Ok(file) = res { sender.input(OpenDialogMsg::FileSelected(file)); } } ), ); } OpenDialogMsg::FileSelected(file) => { self.url.set(file.uri().into()); } OpenDialogMsg::Play => { sender .output(OpenDialogOutput::Play { url: self.url.get().clone(), whisper_stream_index: if self.do_whisper_extraction { self.whisper_stream_index } else { None }, }) .unwrap(); self.dialog.close(); } OpenDialogMsg::SetDoWhisperExtraction(val) => { self.do_whisper_extraction = val; } OpenDialogMsg::WhisperTrackSelected(track_index) => { self.whisper_stream_index = track_index; } } } // once we get all the audio track metadata, we update the whisper track // dropdown fn update_cmd( &mut self, message: Self::CommandOutput, _sender: ComponentSender, _root: &Self::Root, ) { self.metadata_command_running = false; match message { Ok(audio_tracks) => { let list_model = gio::ListStore::new::(); for (&stream_index, track) in audio_tracks.iter() { let track_info = TrackInfo::new( stream_index, track.language.map(|lang| lang.to_name()), track.title.clone(), ); list_model.append(&track_info); } self.whisper_track_selector .sender() .send(TrackSelectorMsg::SetListModel(list_model)) .unwrap(); self.next(); } Err(e) => { let toast = adw::Toast::builder() .title(&format!("Error fetching stream metadata: {}", e)) .build(); self.toast_overlay.as_ref().unwrap().add_toast(toast); } } } } impl OpenDialog { fn reset(&mut self) { self.url.get_mut().clear(); self.do_whisper_extraction = false; self.whisper_stream_index = None; } fn fetch_metadata(&mut self, sender: ComponentSender) { let url = self.url.get().clone(); sender.spawn_oneshot_command(move || { let input = ffmpeg::format::input(&url)?; let audio_tracks = input .streams() .filter_map(|stream| { if stream.parameters().medium() == ffmpeg::media::Type::Audio { Some((stream.index(), TrackMetadata::from_ffmpeg_stream(&stream))) } else { None } }) .collect::>(); Ok(audio_tracks) }); self.metadata_command_running = true; } fn next(&self) { self.navigation_view .as_ref() .unwrap() .push_by_tag("playback_options"); } }