aboutsummaryrefslogtreecommitdiff
path: root/src/open_dialog.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/open_dialog.rs')
-rw-r--r--src/open_dialog.rs328
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 = &gtk::Button {
+ set_label: "Cancel",
+ connect_clicked => OpenDialogMsg::Cancel,
+ },
+
+ pack_end = &gtk::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 = &gtk::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");
+ }
+}