summary refs log tree commit diff
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");
+    }
+}