From 4aa858602d673b761eb33319108843e1eef87bfa Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 16:15:13 +0100 Subject: [PATCH] Replace old track editors with import dialogs --- musicus/src/dialogs/import.rs | 69 +++ musicus/src/dialogs/import_folder.rs | 88 ++++ musicus/src/dialogs/mod.rs | 6 + musicus/src/editors/mod.rs | 10 +- musicus/src/editors/track.rs | 148 ------ musicus/src/editors/track_set.rs | 580 ++++++++++++++++++++++++ musicus/src/editors/track_source.rs | 42 ++ musicus/src/editors/tracks.rs | 337 -------------- musicus/src/meson.build | 9 +- musicus/src/player.rs | 11 +- musicus/src/ripper.rs | 7 - musicus/src/screens/player_screen.rs | 20 +- musicus/src/screens/recording_screen.rs | 48 +- musicus/src/widgets/list.rs | 10 + musicus/src/widgets/player_bar.rs | 8 +- musicus/src/window.rs | 15 +- 16 files changed, 856 insertions(+), 552 deletions(-) create mode 100644 musicus/src/dialogs/import.rs create mode 100644 musicus/src/dialogs/import_folder.rs delete mode 100644 musicus/src/editors/track.rs create mode 100644 musicus/src/editors/track_set.rs create mode 100644 musicus/src/editors/track_source.rs delete mode 100644 musicus/src/editors/tracks.rs diff --git a/musicus/src/dialogs/import.rs b/musicus/src/dialogs/import.rs new file mode 100644 index 0000000..3ab9e03 --- /dev/null +++ b/musicus/src/dialogs/import.rs @@ -0,0 +1,69 @@ +use crate::backend::Backend; +use crate::editors::{TrackSetEditor, TrackSource}; +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; + +/// A dialog for editing metadata while importing music into the music library. +pub struct ImportDialog { + backend: Rc, + source: Rc, + widget: gtk::Box, + navigator: RefCell>>, +} + +impl ImportDialog { + /// Create a new import dialog. + pub fn new(backend: Rc, source: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_dialog.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, add_button); + + let this = Rc::new(Self { + backend, + source, + widget, + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + add_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone()); + navigator.push(editor); + } + })); + + this + } +} + +impl NavigatorScreen for ImportDialog { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/dialogs/import_folder.rs b/musicus/src/dialogs/import_folder.rs new file mode 100644 index 0000000..ca91219 --- /dev/null +++ b/musicus/src/dialogs/import_folder.rs @@ -0,0 +1,88 @@ +use super::ImportDialog; +use crate::backend::Backend; +use crate::editors::TrackSource; +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; +use std::path::Path; + +/// The initial screen for importing a folder. +pub struct ImportFolderDialog { + backend: Rc, + widget: gtk::Box, + navigator: RefCell>>, +} + +impl ImportFolderDialog { + /// Create a new import folderdialog. + pub fn new(backend: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_folder_dialog.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, import_button); + + let this = Rc::new(Self { + backend, + widget, + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + import_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let chooser = gtk::FileChooserNative::new( + Some("Select folder"), + Some(&navigator.window), + gtk::FileChooserAction::SelectFolder, + None, + None, + ); + + chooser.connect_response(clone!(@strong this => move |chooser, response| { + if response == gtk::ResponseType::Accept { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let path = chooser.get_filename().unwrap(); + let source = TrackSource::folder(&path).unwrap(); + let dialog = ImportDialog::new(this.backend.clone(), Rc::new(source)); + navigator.push(dialog); + } + } + })); + + chooser.run(); + } + })); + + this + } +} + +impl NavigatorScreen for ImportFolderDialog { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/dialogs/mod.rs b/musicus/src/dialogs/mod.rs index 2b363ef..6f0a8ae 100644 --- a/musicus/src/dialogs/mod.rs +++ b/musicus/src/dialogs/mod.rs @@ -1,3 +1,9 @@ +pub mod import; +pub use import::*; + +pub mod import_folder; +pub use import_folder::*; + pub mod import_disc; pub use import_disc::*; diff --git a/musicus/src/editors/mod.rs b/musicus/src/editors/mod.rs index e171277..133c0a5 100644 --- a/musicus/src/editors/mod.rs +++ b/musicus/src/editors/mod.rs @@ -10,13 +10,15 @@ pub use person::*; pub mod recording; pub use recording::*; -pub mod tracks; -pub use tracks::*; +pub mod track_set; +pub use track_set::*; + +pub mod track_source; +pub use track_source::*; pub mod work; pub use work::*; mod performance; -mod track; mod work_part; -mod work_section; \ No newline at end of file +mod work_section; diff --git a/musicus/src/editors/track.rs b/musicus/src/editors/track.rs deleted file mode 100644 index 5fdff61..0000000 --- a/musicus/src/editors/track.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::database::*; -use crate::widgets::{Navigator, NavigatorScreen}; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::convert::TryInto; -use std::rc::Rc; - -/// A screen for editing a single track. -// TODO: Refactor. -pub struct TrackEditor { - widget: gtk::Box, - ready_cb: RefCell ()>>>, - navigator: RefCell>>, -} - -impl TrackEditor { - /// Create a new track editor. - pub fn new(track: Track, work: Work) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::ListBox, list); - - let this = Rc::new(Self { - widget, - ready_cb: RefCell::new(None), - navigator: RefCell::new(None), - }); - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - let work = Rc::new(work); - let work_parts = Rc::new(RefCell::new(track.work_parts)); - let file_name = track.file_name; - - save_button.connect_clicked(clone!(@strong this, @strong work_parts => move |_| { - let mut work_parts = work_parts.borrow_mut(); - work_parts.sort(); - - if let Some(cb) = &*this.ready_cb.borrow() { - cb(Track { - work_parts: work_parts.clone(), - file_name: file_name.clone(), - }); - } - - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - for (index, part) in work.parts.iter().enumerate() { - let check = gtk::CheckButton::new(); - check.set_active(work_parts.borrow().contains(&index)); - check.connect_toggled(clone!(@strong check, @strong work_parts => move |_| { - if check.get_active() { - let mut work_parts = work_parts.borrow_mut(); - work_parts.push(index); - } else { - let mut work_parts = work_parts.borrow_mut(); - if let Some(pos) = work_parts.iter().position(|part| *part == index) { - work_parts.remove(pos); - } - } - })); - - let label = gtk::Label::new(Some(&part.title)); - label.set_halign(gtk::Align::Start); - label.set_ellipsize(pango::EllipsizeMode::End); - - let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6); - hbox.set_border_width(6); - hbox.add(&check); - hbox.add(&label); - - let row = gtk::ListBoxRow::new(); - row.add(&hbox); - row.show_all(); - - list.add(&row); - list.connect_row_activated( - clone!(@strong row, @strong check => move |_, activated_row| { - if *activated_row == row { - check.activate(); - } - }), - ); - } - - let mut section_count = 0; - for section in &work.sections { - let attributes = pango::AttrList::new(); - attributes.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap()); - - let label = gtk::Label::new(Some(§ion.title)); - label.set_halign(gtk::Align::Start); - label.set_ellipsize(pango::EllipsizeMode::End); - label.set_attributes(Some(&attributes)); - let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0); - wrap.set_border_width(6); - wrap.add(&label); - - let row = gtk::ListBoxRow::new(); - row.set_activatable(false); - row.add(&wrap); - row.show_all(); - - list.insert( - &row, - (section.before_index + section_count).try_into().unwrap(), - ); - section_count += 1; - } - - this - } - - /// Set the closure to be called when the track was edited. - pub fn set_ready_cb () + 'static>(&self, cb: F) { - self.ready_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for TrackEditor { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/musicus/src/editors/track_set.rs b/musicus/src/editors/track_set.rs new file mode 100644 index 0000000..716fa82 --- /dev/null +++ b/musicus/src/editors/track_set.rs @@ -0,0 +1,580 @@ +use crate::backend::Backend; +use crate::database::{Recording, Track, TrackSet}; +use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector}; +use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::{Cell, RefCell}; +use std::collections::HashSet; +use std::rc::Rc; + +/// Representation of a track that can be imported into the music library. +#[derive(Debug, Clone)] +struct TrackSource { + /// A short string identifying the track for the user. + pub description: String, + + /// Whether the track is ready to be imported. + pub ready: bool, +} + +/// Representation of a medium that can be imported into the music library. +#[derive(Debug, Clone)] +struct MediumSource { + /// The tracks that can be imported from the medium. + pub tracks: Vec, + + /// Whether all tracks are ready to be imported. + pub ready: bool, +} + +impl MediumSource { + /// Create a dummy medium source for testing purposes. + fn dummy() -> Self { + let mut tracks = Vec::new(); + + for index in 0..20 { + tracks.push(TrackSource { + description: format!("Track {}", index + 1), + ready: Cell::new(true), + }); + } + + Self { + tracks, + ready: Cell::new(true), + } + } +} + +/// A track while being edited. +#[derive(Debug, Clone)] +struct TrackData<'a> { + /// A reference to the selected track source. + pub source: &'a TrackSource, + + /// The actual value for the track. + pub track: Track, +} + +/// A track set while being edited. +#[derive(Debug, Clone)] +struct TrackSetData<'a> { + /// The recording to which the tracks belong. + pub recording: Option, + + /// The tracks that are being edited. + pub tracks: Vec>, +} + +impl TrackSetData { + /// Create a new empty track set. + pub fn new() -> Self { + Self { + recording: None, + tracks: Vec::new(), + } + } +} + +/// A screen for editing a set of tracks for one recording. +pub struct TrackSetEditor { + backend: Rc, + source: Rc>, + widget: gtk::Box, + save_button: gtk::Button, + recording_row: libhandy::ActionRow, + track_list: List, + data: RefCell, + done_cb: RefCell>>, + navigator: RefCell>>, +} + +impl TrackSetEditor { + /// Create a new track set editor. + pub fn new(backend: Rc, source: Rc) -> Rc { + // TODO: Replace with argument. + let source = Rc::new(RefCell::new(MediumSource::dummy())); + + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, save_button); + get_widget!(builder, libhandy::ActionRow, recording_row); + get_widget!(builder, gtk::Button, select_recording_button); + get_widget!(builder, gtk::Button, edit_tracks_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = List::new(&gettext!("No tracks added")); + tracks_frame.add(&track_list.widget); + + let this = Rc::new(Self { + backend, + source, + widget, + save_button, + recording_row, + track_list, + data: RefCell::new(TrackSetData::new()), + done_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + this.save_button.connect_clicked(clone!(@strong this => move |_| { + if let Some(cb) = &*this.done_cb.borrow() {} + })); + + select_recording_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let person_selector = PersonSelector::new(this.backend.clone()); + + person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); + + work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { + let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone()); + + recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { + let mut data = this.data.borrow_mut(); + data.recording = Some(recording); + this.recording_selected(); + + navigator.clone().pop(); + navigator.clone().pop(); + navigator.clone().pop(); + })); + + navigator.clone().push(recording_selector); + })); + + navigator.clone().push(work_selector); + })); + + navigator.clone().push(person_selector); + } + })); + + edit_tracks_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = TrackSelector::new(Rc::clone(this.source)); + + selector.set_selected_cb(clone!(@strong this => move |selection| { + let mut tracks = Vec::new(); + + for index in selection { + let track = Track { + work_parts: Vec::new(), + }; + + let source = this.source.tracks[index].clone(); + + let data = TrackData { + track, + source, + }; + + tracks.push(data); + } + + let length = tracks.len(); + this.tracks.replace(tracks); + this.track_list.update(length); + this.autofill_parts(); + })); + + navigator.push(selector); + } + })); + + this.track_list.set_make_widget(clone!(@strong this => move |index| { + let data = &this.tracks.borrow()[index]; + + let mut title_parts = Vec::::new(); + + if let Some(recording) = &*this.recording.borrow() { + for part in &data.track.work_parts { + title_parts.push(recording.work.parts[*part].title.clone()); + } + } + + let title = if title_parts.is_empty() { + gettext("Unknown") + } else { + title_parts.join(", ") + }; + + let subtitle = data.source.description.clone(); + + let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + edit_button.set_valign(gtk::Align::Center); + edit_button.add(&edit_image); + + let row = libhandy::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&title)); + row.set_subtitle(Some(&subtitle)); + row.add(&edit_button); + row.set_activatable_widget(Some(&edit_button)); + row.show_all(); + + edit_button.connect_clicked(clone!(@strong this => move |_| { + let recording = this.recording.borrow().clone(); + let navigator = this.navigator.borrow().clone(); + + if let (Some(recording), Some(navigator)) = (recording, navigator) { + let editor = TrackEditor::new(recording, Vec::new()); + + editor.set_selected_cb(clone!(@strong this => move |selection| { + { + let mut tracks = &mut this.data.borrow_mut().tracks; + let mut track = &mut tracks[index]; + track.track.work_parts = selection; + }; + + this.update_tracks(); + })); + + navigator.push(editor); + } + })); + + row.upcast() + })); + + this + } + + /// Set the closure to be called when the user has created the track set. + pub fn set_done_cb(&self, cb: F) { + self.done_cb.replace(Some(Box::new(cb))); + } + + /// Set everything up after selecting a recording. + fn recording_selected(&self) { + if let Some(recording) = self.data.borrow().recording { + self.recording_row.set_title(Some(&recording.work.get_title())); + self.recording_row.set_subtitle(Some(&recording.get_performers())); + self.save_button.set_sensitive(true); + } + + self.autofill_parts(); + } + + /// Automatically try to put work part information from the selected recording into the + /// selected tracks. + fn autofill_parts(&self) { + if let Some(recording) = self.data.borrow().recording { + let mut tracks = self.tracks.borrow_mut(); + + for (index, _) in recording.work.parts.iter().enumerate() { + if let Some(mut data) = tracks.get_mut(index) { + data.track.work_parts = vec![index]; + } else { + break; + } + } + } + + self.update_tracks(); + } + + /// Update the track list. + fn update_tracks(&self) { + let length = self.data.borrow().tracks.len(); + self.track_list.update(length); + } +} + +impl NavigatorScreen for TrackSetEditor { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} + + +/// A screen for selecting tracks from a medium. +struct TrackSelector { + source: Rc>, + widget: gtk::Box, + select_button: gtk::Button, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackSelector { + /// Create a new track selector. + pub fn new(source: Rc>) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = gtk::ListBox::new(); + track_list.set_selection_mode(gtk::SelectionMode::None); + track_list.set_vexpand(false); + track_list.show(); + tracks_frame.add(&track_list); + + let this = Rc::new(Self { + source, + widget, + select_button, + selection: RefCell::new(Vec::new()), + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + this.select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, track) in this.tracks.iter().enumerate() { + let check = gtk::CheckButton::new(); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + + if selection.is_empty() { + this.select_button.set_sensitive(false); + } else { + this.select_button.set_sensitive(true); + } + })); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&track.description)); + row.show_all(); + + track_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has selected tracks. The + /// closure will be called with the indices of the selected tracks as its + /// argument. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} + +/// A screen for editing a single track. +struct TrackEditor { + widget: gtk::Box, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackEditor { + /// Create a new track editor. + pub fn new(recording: Recording, selection: Vec) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, parts_frame); + + let parts_list = gtk::ListBox::new(); + parts_list.set_selection_mode(gtk::SelectionMode::None); + parts_list.set_vexpand(false); + parts_list.show(); + parts_frame.add(&parts_list); + + let this = Rc::new(Self { + widget, + selection: RefCell::new(selection), + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, part) in recording.work.parts.iter().enumerate() { + let check = gtk::CheckButton::new(); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + })); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&part.title)); + row.show_all(); + + parts_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has edited the track. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackEditor { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} + +/// A simple list of widgets. +struct List { + pub widget: gtk::ListBox, + make_widget: RefCell gtk::Widget>>>, +} + +impl List { + /// Create a new list. The list will be empty. + pub fn new(placeholder_text: &str) -> Self { + let placeholder_label = gtk::Label::new(Some(placeholder_text)); + placeholder_label.set_margin_top(6); + placeholder_label.set_margin_bottom(6); + placeholder_label.set_margin_start(6); + placeholder_label.set_margin_end(6); + placeholder_label.show(); + + let widget = gtk::ListBox::new(); + widget.set_selection_mode(gtk::SelectionMode::None); + widget.set_placeholder(Some(&placeholder_label)); + widget.show(); + + Self { + widget, + make_widget: RefCell::new(None), + } + } + + /// Set the closure to be called to construct widgets for the items. + pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { + self.make_widget.replace(Some(Box::new(make_widget))); + } + + /// Call the make_widget function for each item. This will automatically + /// show all children by indices 0..length. + pub fn update(&self, length: usize) { + for child in self.widget.get_children() { + self.widget.remove(&child); + } + + if let Some(make_widget) = &*self.make_widget.borrow() { + for index in 0..length { + let row = make_widget(index); + self.widget.insert(&row, -1); + } + } + } +} diff --git a/musicus/src/editors/track_source.rs b/musicus/src/editors/track_source.rs new file mode 100644 index 0000000..3f09d05 --- /dev/null +++ b/musicus/src/editors/track_source.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use std::cell::Cell; +use std::path::Path; + +/// One track within a [`TrackSource`]. +#[derive(Debug, Clone)] +pub struct TrackState { + pub description: String, +} + +/// A live representation of a source of audio tracks. +pub struct TrackSource { + pub tracks: Vec, + pub ready: Cell, +} + +impl TrackSource { + /// Create a new track source for a folder. This will provide the folder's + /// files as selectable tracks and be ready immediately. + pub fn folder(path: &Path) -> Result { + let mut tracks = Vec::::new(); + + let entries = std::fs::read_dir(path)?; + for entry in entries { + let entry = entry?; + if entry.file_type()?.is_file() { + let file_name = entry.file_name(); + let track = TrackState { description: file_name.to_str().unwrap().to_owned() }; + tracks.push(track); + } + } + + tracks.sort_unstable_by(|a, b| { + a.description.cmp(&b.description) + }); + + Ok(Self { + tracks, + ready: Cell::new(true), + }) + } +} diff --git a/musicus/src/editors/tracks.rs b/musicus/src/editors/tracks.rs deleted file mode 100644 index b373b40..0000000 --- a/musicus/src/editors/tracks.rs +++ /dev/null @@ -1,337 +0,0 @@ -use super::track::TrackEditor; -use crate::backend::Backend; -use crate::database::*; -use crate::widgets::{List, Navigator, NavigatorScreen}; -use crate::selectors::{PersonSelector, WorkSelector, RecordingSelector}; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for editing a set of tracks. -// TODO: Disable buttons if no track is selected. -pub struct TracksEditor { - backend: Rc, - widget: gtk::Box, - save_button: gtk::Button, - recording_stack: gtk::Stack, - work_label: gtk::Label, - performers_label: gtk::Label, - track_list: Rc>, - recording: RefCell>, - tracks: RefCell>, - callback: RefCell ()>>>, - navigator: RefCell>>, -} - -impl TracksEditor { - /// Create a new track editor an optionally initialize it with a recording and a list of - /// tracks. - pub fn new( - backend: Rc, - recording: Option, - tracks: Vec, - ) -> Rc { - // UI setup - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::Button, recording_button); - get_widget!(builder, gtk::Stack, recording_stack); - get_widget!(builder, gtk::Label, work_label); - get_widget!(builder, gtk::Label, performers_label); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, add_track_button); - get_widget!(builder, gtk::Button, edit_track_button); - get_widget!(builder, gtk::Button, remove_track_button); - get_widget!(builder, gtk::Button, move_track_up_button); - get_widget!(builder, gtk::Button, move_track_down_button); - - let track_list = List::new(&gettext("Add some tracks.")); - scroll.add(&track_list.widget); - - let this = Rc::new(Self { - backend, - widget, - save_button, - recording_stack, - work_label, - performers_label, - track_list, - recording: RefCell::new(recording), - tracks: RefCell::new(tracks), - callback: RefCell::new(None), - navigator: RefCell::new(None), - }); - - // Signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - this.save_button - .connect_clicked(clone!(@strong this => move |_| { - let context = glib::MainContext::default(); - let this = this.clone(); - context.spawn_local(async move { - let recording = this.recording.borrow().as_ref().unwrap().clone(); - - // Add the recording first, if it's from the server. - - if !this.backend.db().recording_exists(&recording.id).await.unwrap() { - this.backend.db().update_recording(recording.clone()).await.unwrap(); - } - - // Add the actual tracks. - - this.backend.db().update_tracks( - &recording.id, - this.tracks.borrow().clone(), - ).await.unwrap(); - - if let Some(callback) = &*this.callback.borrow() { - callback(); - } - - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - }); - - })); - - recording_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let person_selector = PersonSelector::new(this.backend.clone()); - - person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { - let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); - - work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { - let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone()); - - recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { - this.recording_selected(recording); - this.recording.replace(Some(recording.clone())); - - navigator.clone().pop(); - navigator.clone().pop(); - navigator.clone().pop(); - })); - - navigator.clone().push(recording_selector); - })); - - navigator.clone().push(work_selector); - })); - - navigator.clone().push(person_selector); - } - })); - - this.track_list - .set_make_widget(clone!(@strong this => move |track| { - this.build_track_row(track) - })); - - add_track_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let music_library_path = this.backend.get_music_library_path().unwrap(); - - let dialog = gtk::FileChooserNative::new( - Some(&gettext("Select audio files")), - Some(&navigator.window), - gtk::FileChooserAction::Open, - None, - None, - ); - - dialog.set_select_multiple(true); - dialog.set_current_folder(&music_library_path); - - if let gtk::ResponseType::Accept = dialog.run() { - let mut index = match this.track_list.get_selected_index() { - Some(index) => index + 1, - None => this.tracks.borrow().len(), - }; - - { - let mut tracks = this.tracks.borrow_mut(); - for file_name in dialog.get_filenames() { - let file_name = file_name.strip_prefix(&music_library_path).unwrap(); - tracks.insert(index, Track { - work_parts: Vec::new(), - file_name: String::from(file_name.to_str().unwrap()), - }); - index += 1; - } - } - - this.track_list.show_items(this.tracks.borrow().clone()); - this.autofill_parts(); - this.track_list.select_index(index); - } - } - })); - - remove_track_button.connect_clicked(clone!(@strong this => move |_| { - match this.track_list.get_selected_index() { - Some(index) => { - let mut tracks = this.tracks.borrow_mut(); - tracks.remove(index); - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index); - } - None => (), - } - })); - - move_track_up_button.connect_clicked(clone!(@strong this => move |_| { - match this.track_list.get_selected_index() { - Some(index) => { - if index > 0 { - let mut tracks = this.tracks.borrow_mut(); - tracks.swap(index - 1, index); - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index - 1); - } - } - None => (), - } - })); - - move_track_down_button.connect_clicked(clone!(@strong this => move |_| { - match this.track_list.get_selected_index() { - Some(index) => { - let mut tracks = this.tracks.borrow_mut(); - if index < tracks.len() - 1 { - tracks.swap(index, index + 1); - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index + 1); - } - } - None => (), - } - })); - - edit_track_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - if let Some(index) = this.track_list.get_selected_index() { - if let Some(recording) = &*this.recording.borrow() { - let editor = TrackEditor::new(this.tracks.borrow()[index].clone(), recording.work.clone()); - - editor.set_ready_cb(clone!(@strong this => move |track| { - let mut tracks = this.tracks.borrow_mut(); - tracks[index] = track; - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index); - })); - - navigator.push(editor); - } - } - } - })); - - // Initialization - - if let Some(recording) = &*this.recording.borrow() { - this.recording_selected(recording); - } - - this.track_list.show_items(this.tracks.borrow().clone()); - - this - } - - /// Set a callback to be called when the tracks are saved. - pub fn set_callback () + 'static>(&self, cb: F) { - self.callback.replace(Some(Box::new(cb))); - } - - /// Create a widget representing a track. - fn build_track_row(&self, track: &Track) -> gtk::Widget { - let mut title_parts = Vec::::new(); - for part in &track.work_parts { - if let Some(recording) = &*self.recording.borrow() { - title_parts.push(recording.work.parts[*part].title.clone()); - } - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let title_label = gtk::Label::new(Some(&title)); - title_label.set_ellipsize(pango::EllipsizeMode::End); - title_label.set_halign(gtk::Align::Start); - - let file_name_label = gtk::Label::new(Some(&track.file_name)); - file_name_label.set_ellipsize(pango::EllipsizeMode::End); - file_name_label.set_opacity(0.5); - file_name_label.set_halign(gtk::Align::Start); - - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&title_label); - vbox.add(&file_name_label); - - vbox.upcast() - } - - /// Set everything up after selecting a recording. - fn recording_selected(&self, recording: &Recording) { - self.work_label.set_text(&recording.work.get_title()); - self.performers_label.set_text(&recording.get_performers()); - self.recording_stack.set_visible_child_name("selected"); - self.save_button.set_sensitive(true); - self.autofill_parts(); - } - - /// Automatically try to put work part information from the selected recording into the - /// selected tracks. - fn autofill_parts(&self) { - if let Some(recording) = &*self.recording.borrow() { - let mut tracks = self.tracks.borrow_mut(); - - for (index, _) in recording.work.parts.iter().enumerate() { - if let Some(mut track) = tracks.get_mut(index) { - track.work_parts = vec![index]; - } else { - break; - } - } - - self.track_list.show_items(tracks.clone()); - } - } -} - -impl NavigatorScreen for TracksEditor { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/musicus/src/meson.build b/musicus/src/meson.build index f91c62e..363f0ee 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -33,9 +33,9 @@ run_command( ) sources = files( - 'backend/client/mod.rs', 'backend/client/ensembles.rs', 'backend/client/instruments.rs', + 'backend/client/mod.rs', 'backend/client/persons.rs', 'backend/client/recordings.rs', 'backend/client/works.rs', @@ -43,16 +43,18 @@ sources = files( 'backend/mod.rs', 'backend/secure.rs', 'database/ensembles.rs', + 'database/files.rs', 'database/instruments.rs', + 'database/medium.rs', 'database/mod.rs', 'database/persons.rs', 'database/recordings.rs', 'database/schema.rs', 'database/thread.rs', - 'database/tracks.rs', 'database/works.rs', 'dialogs/about.rs', 'dialogs/import_disc.rs', + 'dialogs/import_folder.rs', 'dialogs/login_dialog.rs', 'dialogs/mod.rs', 'dialogs/preferences.rs', @@ -63,8 +65,7 @@ sources = files( 'editors/performance.rs', 'editors/person.rs', 'editors/recording.rs', - 'editors/track.rs', - 'editors/tracks.rs', + 'editors/track_set.rs', 'editors/work.rs', 'editors/work_part.rs', 'editors/work_section.rs', diff --git a/musicus/src/player.rs b/musicus/src/player.rs index 6d1a294..f59371b 100644 --- a/musicus/src/player.rs +++ b/musicus/src/player.rs @@ -9,6 +9,7 @@ use std::rc::Rc; #[derive(Clone)] pub struct PlaylistItem { pub tracks: TrackSet, + pub file_names: Vec, pub indices: Vec, } @@ -248,15 +249,7 @@ impl Player { "file://{}", self.music_library_path .join( - self.playlist - .borrow() - .get(current_item) - .ok_or(anyhow!("Playlist item doesn't exist!"))? - .tracks - .get(current_track) - .ok_or(anyhow!("Track doesn't exist!"))? - .file_name - .clone(), + self.playlist.borrow()[current_item].file_names[current_track].clone(), ) .to_str() .unwrap(), diff --git a/musicus/src/ripper.rs b/musicus/src/ripper.rs index 2b3f22f..bbdbde5 100644 --- a/musicus/src/ripper.rs +++ b/musicus/src/ripper.rs @@ -104,13 +104,6 @@ impl Ripper { /// Build the GStreamer pipeline to rip a track. fn build_pipeline(path: &str, track: u32) -> Result { let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?; - - // // TODO: Remove. - // cdparanoiasrc.set_property( - // "device", - // &String::from("/home/johrpan/Diverses/arrau_schumann.iso"), - // )?; - cdparanoiasrc.set_property("track", &track)?; let queue = ElementFactory::make("queue", None)?; diff --git a/musicus/src/screens/player_screen.rs b/musicus/src/screens/player_screen.rs index 3264e7d..8a258e2 100644 --- a/musicus/src/screens/player_screen.rs +++ b/musicus/src/screens/player_screen.rs @@ -215,15 +215,17 @@ impl PlayerScreen { elements.push(PlaylistElement { item: item_index, track: 0, - title: item.recording.work.get_title(), - subtitle: Some(item.recording.get_performers()), + title: item.tracks.recording.work.get_title(), + subtitle: Some(item.tracks.recording.get_performers()), playable: false, }); - for (track_index, track) in item.tracks.iter().enumerate() { + for track_index in &item.indices { + let track = &item.tracks.tracks[*track_index]; + let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.recording.work.parts[*part].title.clone()); + parts.push(item.tracks.recording.work.parts[*part].title.clone()); } let title = if parts.is_empty() { @@ -234,7 +236,7 @@ impl PlayerScreen { elements.push(PlaylistElement { item: item_index, - track: track_index, + track: *track_index, title: title, subtitle: None, playable: true, @@ -262,20 +264,20 @@ impl PlayerScreen { next_button.set_sensitive(player.has_next()); let item = &playlist.borrow()[current_item]; - let track = &item.tracks[current_track]; + let track = &item.tracks.tracks[current_track]; let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.recording.work.parts[*part].title.clone()); + parts.push(item.tracks.recording.work.parts[*part].title.clone()); } - let mut title = item.recording.work.get_title(); + let mut title = item.tracks.recording.work.get_title(); if !parts.is_empty() { title = format!("{}: {}", title, parts.join(", ")); } title_label.set_text(&title); - subtitle_label.set_text(&item.recording.get_performers()); + subtitle_label.set_text(&item.tracks.recording.get_performers()); position_label.set_text("0:00"); self_item.replace(current_item); diff --git a/musicus/src/screens/recording_screen.rs b/musicus/src/screens/recording_screen.rs index fa0789e..ec94fdb 100644 --- a/musicus/src/screens/recording_screen.rs +++ b/musicus/src/screens/recording_screen.rs @@ -1,6 +1,6 @@ use crate::backend::*; use crate::database::*; -use crate::editors::{RecordingEditor, TracksEditor}; +use crate::editors::RecordingEditor; use crate::player::*; use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use gettextrs::gettext; @@ -76,15 +76,15 @@ impl RecordingScreen { title_label.set_ellipsize(pango::EllipsizeMode::End); title_label.set_halign(gtk::Align::Start); - let file_name_label = gtk::Label::new(Some(&track.file_name)); - file_name_label.set_ellipsize(pango::EllipsizeMode::End); - file_name_label.set_opacity(0.5); - file_name_label.set_halign(gtk::Align::Start); + // let file_name_label = gtk::Label::new(Some(&track.file_name)); + // file_name_label.set_ellipsize(pango::EllipsizeMode::End); + // file_name_label.set_opacity(0.5); + // file_name_label.set_halign(gtk::Align::Start); let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); vbox.set_border_width(6); vbox.add(&title_label); - vbox.add(&file_name_label); + // vbox.add(&file_name_label); vbox.upcast() })); @@ -98,10 +98,10 @@ impl RecordingScreen { add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| { if let Some(player) = result.backend.get_player() { - player.add_item(PlaylistItem { - recording: result.recording.clone(), - tracks: result.tracks.borrow().clone(), - }).unwrap(); + // player.add_item(PlaylistItem { + // recording: result.recording.clone(), + // tracks: result.tracks.borrow().clone(), + // }).unwrap(); } })); @@ -121,33 +121,33 @@ impl RecordingScreen { })); edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| { - let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone()); - let window = NavigatorWindow::new(editor); - window.show(); + // let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone()); + // let window = NavigatorWindow::new(editor); + // window.show(); })); delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| { let context = glib::MainContext::default(); let clone = result.clone(); context.spawn_local(async move { - clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap(); - clone.backend.library_changed(); + // clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap(); + // clone.backend.library_changed(); }); })); let context = glib::MainContext::default(); let clone = result.clone(); context.spawn_local(async move { - let tracks = clone - .backend - .db() - .get_tracks(&clone.recording.id) - .await - .unwrap(); + // let tracks = clone + // .backend + // .db() + // .get_tracks(&clone.recording.id) + // .await + // .unwrap(); - list.show_items(tracks.clone()); - clone.stack.set_visible_child_name("content"); - clone.tracks.replace(tracks); + // list.show_items(tracks.clone()); + // clone.stack.set_visible_child_name("content"); + // clone.tracks.replace(tracks); }); result diff --git a/musicus/src/widgets/list.rs b/musicus/src/widgets/list.rs index e781f17..890f866 100644 --- a/musicus/src/widgets/list.rs +++ b/musicus/src/widgets/list.rs @@ -63,6 +63,16 @@ where this } + pub fn set_selectable(&self, selectable: bool) { + let mode = if selectable { + gtk::SelectionMode::Single + } else { + gtk::SelectionMode::None + }; + + self.widget.set_selection_mode(mode); + } + pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { self.make_widget.replace(Some(Box::new(make_widget))); } diff --git a/musicus/src/widgets/player_bar.rs b/musicus/src/widgets/player_bar.rs index 9d2cf90..c84d11b 100644 --- a/musicus/src/widgets/player_bar.rs +++ b/musicus/src/widgets/player_bar.rs @@ -112,20 +112,20 @@ impl PlayerBar { next_button.set_sensitive(player.has_next()); let item = &playlist.borrow()[current_item]; - let track = &item.tracks[current_track]; + let track = &item.tracks.tracks[current_track]; let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.recording.work.parts[*part].title.clone()); + parts.push(item.tracks.recording.work.parts[*part].title.clone()); } - let mut title = item.recording.work.get_title(); + let mut title = item.tracks.recording.work.get_title(); if !parts.is_empty() { title = format!("{}: {}", title, parts.join(", ")); } title_label.set_text(&title); - subtitle_label.set_text(&item.recording.get_performers()); + subtitle_label.set_text(&item.tracks.recording.get_performers()); position_label.set_text("0:00"); } )); diff --git a/musicus/src/window.rs b/musicus/src/window.rs index 9cd4bc7..2d3224c 100644 --- a/musicus/src/window.rs +++ b/musicus/src/window.rs @@ -1,6 +1,5 @@ use crate::backend::*; use crate::dialogs::*; -use crate::editors::TracksEditor; use crate::screens::*; use crate::widgets::*; use futures::prelude::*; @@ -85,13 +84,17 @@ impl Window { })); add_button.connect_clicked(clone!(@strong result => move |_| { - let editor = TracksEditor::new(result.backend.clone(), None, Vec::new()); + // let editor = TracksEditor::new(result.backend.clone(), None, Vec::new()); - editor.set_callback(clone!(@strong result => move || { - result.reload(); - })); + // editor.set_callback(clone!(@strong result => move || { + // result.reload(); + // })); - let window = NavigatorWindow::new(editor); + // let window = NavigatorWindow::new(editor); + // window.show(); + + let dialog = ImportFolderDialog::new(result.backend.clone()); + let window = NavigatorWindow::new(dialog); window.show(); }));