diff --git a/src/import/disc_source.rs b/src/import/disc_source.rs index 1c3f479..49707e5 100644 --- a/src/import/disc_source.rs +++ b/src/import/disc_source.rs @@ -3,6 +3,7 @@ use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use discid::DiscId; use futures_channel::oneshot; +use gettextrs::gettext; use gstreamer::prelude::*; use gstreamer::{Element, ElementFactory, Pipeline}; use once_cell::sync::OnceCell; @@ -45,6 +46,8 @@ impl DiscSource { let tmp_dir = Self::create_tmp_dir()?; for number in first_track..=last_track { + let name = gettext!("Track {}", number); + let file_name = format!("track_{:02}.flac", number); let mut path = tmp_dir.clone(); @@ -52,6 +55,7 @@ impl DiscSource { let track = SourceTrack { number, + name, path, }; diff --git a/src/import/folder_source.rs b/src/import/folder_source.rs new file mode 100644 index 0000000..b0637d5 --- /dev/null +++ b/src/import/folder_source.rs @@ -0,0 +1,90 @@ +use super::source::{Source, SourceTrack}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures_channel::oneshot; +use once_cell::sync::OnceCell; +use std::path::{Path, PathBuf}; +use std::thread; + +/// A folder outside of the music library that contains tracks to import. +#[derive(Clone, Debug)] +pub struct FolderSource { + /// The path to the folder. + path: PathBuf, + + /// The tracks within the folder. + tracks: OnceCell>, +} + +impl FolderSource { + /// Create a new folder source. + pub fn new(path: PathBuf) -> Self { + Self { + path, + tracks: OnceCell::new(), + } + } + + /// Load the contents of the folder as tracks. + fn load_priv(path: &Path) -> Result> { + let mut tracks = Vec::new(); + let mut number = 1; + + for entry in std::fs::read_dir(path)? { + let entry = entry?; + + if entry.file_type()?.is_file() { + let name = entry + .file_name() + .into_string() + .or_else(|_| Err(anyhow!("Failed to convert OsString to String!")))?; + + let path = entry.path(); + + let track = SourceTrack { + number, + name, + path, + }; + + tracks.push(track); + number += 1; + } + } + + Ok(tracks) + } +} + +#[async_trait] +impl Source for FolderSource { + async fn load(&self) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + + let path = self.path.clone(); + thread::spawn(move || { + let result = Self::load_priv(&path); + sender.send(result).unwrap(); + }); + + let tracks = receiver.await??; + self.tracks.set(tracks); + + Ok(()) + } + + fn tracks(&self) -> Option<&[SourceTrack]> { + match self.tracks.get() { + Some(tracks) => Some(tracks.as_slice()), + None => None, + } + } + + fn discid(&self) -> Option { + None + } + + async fn copy(&self) -> Result<()> { + Ok(()) + } +} diff --git a/src/import/mod.rs b/src/import/mod.rs index a28f6d1..f21da67 100644 --- a/src/import/mod.rs +++ b/src/import/mod.rs @@ -1,4 +1,5 @@ mod disc_source; +mod folder_source; mod medium_editor; mod source; mod source_selector; diff --git a/src/import/source.rs b/src/import/source.rs index 7020bae..dfcd4d0 100644 --- a/src/import/source.rs +++ b/src/import/source.rs @@ -27,6 +27,10 @@ pub struct SourceTrack { /// the track numbers start. pub number: u32, + /// A human readable identifier for the track. This will be used to + /// present the track for selection. + pub name: String, + /// The path to the file where the corresponding audio file is. This file /// is only required to exist, once the source's copy method has finished. /// This will not be the actual file within the user's music library, but diff --git a/src/import/source_selector.rs b/src/import/source_selector.rs index ac4974c..da95ba3 100644 --- a/src/import/source_selector.rs +++ b/src/import/source_selector.rs @@ -1,12 +1,15 @@ use super::medium_editor::MediumEditor; use super::disc_source::DiscSource; +use super::folder_source::FolderSource; use super::source::Source; use crate::backend::Backend; use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; use std::cell::RefCell; +use std::path::PathBuf; use std::rc::Rc; /// A dialog for starting to import music. @@ -49,6 +52,55 @@ impl SourceSelector { } })); + folder_button.connect_clicked(clone!(@strong this => move |_| { + let window = this.navigator.borrow().clone().unwrap().window.clone(); + let dialog = gtk::FileChooserDialog::new( + Some(&gettext("Select folder")), + Some(&window), + gtk::FileChooserAction::SelectFolder, + &[ + (&gettext("Cancel"), gtk::ResponseType::Cancel), + (&gettext("Select"), gtk::ResponseType::Accept), + ]); + + dialog.connect_response(clone!(@strong this => move |dialog, response| { + this.stack.set_visible_child_name("loading"); + dialog.hide(); + + if let gtk::ResponseType::Accept = response { + if let Some(file) = dialog.get_file() { + if let Some(path) = file.get_path() { + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + let folder = FolderSource::new(PathBuf::from(path)); + match folder.load().await { + Ok(_) => { + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let source = Rc::new(Box::new(folder) as Box); + let editor = MediumEditor::new(clone.backend.clone(), source); + navigator.push(editor); + } + + clone.info_bar.set_revealed(false); + clone.stack.set_visible_child_name("start"); + } + Err(_) => { + // TODO: Present error. + clone.info_bar.set_revealed(true); + clone.stack.set_visible_child_name("start"); + } + } + }); + } + } + } + })); + + dialog.show(); + })); + disc_button.connect_clicked(clone!(@strong this => move |_| { this.stack.set_visible_child_name("loading"); @@ -69,6 +121,7 @@ impl SourceSelector { clone.stack.set_visible_child_name("start"); } Err(_) => { + // TODO: Present error. clone.info_bar.set_revealed(true); clone.stack.set_visible_child_name("start"); } diff --git a/src/import/track_selector.rs b/src/import/track_selector.rs index 4059fc9..068fa73 100644 --- a/src/import/track_selector.rs +++ b/src/import/track_selector.rs @@ -87,13 +87,11 @@ impl TrackSelector { } })); - let title = format!("Track {}", track.number); - let row = libadwaita::ActionRow::new(); row.add_prefix(&check); row.set_activatable_widget(Some(&check)); row.set_activatable(true); - row.set_title(Some(&title)); + row.set_title(Some(&track.name)); track_list.append(&row); } diff --git a/src/import/track_set_editor.rs b/src/import/track_set_editor.rs index 7381a1f..ed79c70 100644 --- a/src/import/track_set_editor.rs +++ b/src/import/track_set_editor.rs @@ -175,9 +175,7 @@ impl TrackSetEditor { }; let tracks = this.source.tracks().unwrap(); - - let number = tracks[track.track_source].number; - let subtitle = format!("Track {}", number); + let track_name = &tracks[track.track_source].name; let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic")); let edit_button = gtk::Button::new(); @@ -188,7 +186,7 @@ impl TrackSetEditor { let row = libadwaita::ActionRow::new(); row.set_activatable(true); row.set_title(Some(&title)); - row.set_subtitle(Some(&subtitle)); + row.set_subtitle(Some(track_name)); row.add_suffix(&edit_button); row.set_activatable_widget(Some(&edit_button)); diff --git a/src/meson.build b/src/meson.build index 53a82b3..0091060 100644 --- a/src/meson.build +++ b/src/meson.build @@ -66,6 +66,7 @@ sources = files( 'editors/work_part.rs', 'editors/work_section.rs', 'import/disc_source.rs', + 'import/folder_source.rs', 'import/medium_editor.rs', 'import/mod.rs', 'import/source.rs',