diff options
Diffstat (limited to 'src/open_dialog.rs')
| -rw-r--r-- | src/open_dialog.rs | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/src/open_dialog.rs b/src/open_dialog.rs new file mode 100644 index 0000000..2f17c59 --- /dev/null +++ b/src/open_dialog.rs @@ -0,0 +1,328 @@ +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<adw::ToastOverlay>, + navigation_view: Option<adw::NavigationView>, + whisper_track_selector: Controller<TrackSelector>, + + url: Tracker<String>, + do_whisper_extraction: bool, + whisper_stream_index: Option<StreamIndex>, + + metadata_command_running: bool, +} + +#[derive(Debug)] +pub enum OpenDialogMsg { + Show, + Next, + Cancel, + SelectFile, + FileSelected(gio::File), + UrlChanged(String), + SetDoWhisperExtraction(bool), + WhisperTrackSelected(Option<StreamIndex>), + Play, +} + +#[derive(Debug)] +pub enum OpenDialogOutput { + Play { + url: String, + whisper_stream_index: Option<StreamIndex>, + }, +} + +#[relm4::component(pub)] +impl Component for OpenDialog { + type Init = adw::ApplicationWindow; + type Input = OpenDialogMsg; + type Output = OpenDialogOutput; + type CommandOutput = Result<BTreeMap<StreamIndex, TrackMetadata>, 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<Self>, + ) -> ComponentParts<Self> { + 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<Self>, _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<Self>, + _root: &Self::Root, + ) { + self.metadata_command_running = false; + + match message { + Ok(audio_tracks) => { + let list_model = gio::ListStore::new::<TrackInfo>(); + + 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<Self>) { + 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::<BTreeMap<_, _>>(); + + Ok(audio_tracks) + }); + + self.metadata_command_running = true; + } + + fn next(&self) { + self.navigation_view + .as_ref() + .unwrap() + .push_by_tag("playback_options"); + } +} |