diff --git a/res/resources.xml b/res/resources.xml index d59dcd3..51b79bd 100644 --- a/res/resources.xml +++ b/res/resources.xml @@ -18,6 +18,8 @@ ui/recording_selector.ui ui/recording_selector_screen.ui ui/section_editor.ui + ui/tracks_editor.ui + ui/track_editor.ui ui/window.ui ui/work_editor.ui ui/work_screen.ui diff --git a/res/ui/track_editor.ui b/res/ui/track_editor.ui new file mode 100644 index 0000000..eac3a38 --- /dev/null +++ b/res/ui/track_editor.ui @@ -0,0 +1,69 @@ + + + + + + False + True + 350 + 200 + dialog + + + True + True + + + True + False + none + + + True + False + none + + + True + False + Select a recording of a work with multiple parts. + + + + + + + + + + + True + False + Track + + + Cancel + True + True + True + + + + + Save + True + True + True + + + + end + 1 + + + + + + diff --git a/res/ui/tracks_editor.ui b/res/ui/tracks_editor.ui new file mode 100644 index 0000000..94168aa --- /dev/null +++ b/res/ui/tracks_editor.ui @@ -0,0 +1,280 @@ + + + + + + False + True + 400 + 300 + dialog + + + + True + False + 18 + 12 + 6 + + + True + False + end + Recording + + + 0 + 0 + + + + + True + True + True + True + + + True + False + False + crossfade + True + + + True + False + start + Select … + + + unselected + + + + + True + False + vertical + + + True + False + start + Work + end + + + + + + False + True + 0 + + + + + True + False + 0.5 + start + Performers + end + + + False + True + 1 + + + + + selected + 1 + + + + + + + 1 + 0 + + + + + True + False + True + 6 + + + True + True + in + + + + + + True + True + 0 + + + + + True + False + vertical + 6 + + + True + True + True + + + True + False + list-add-symbolic + + + + + False + True + 0 + + + + + True + True + True + + + True + False + edit-symbolic + + + + + False + True + 2 + + + + + True + True + True + + + True + False + list-remove-symbolic + + + + + False + True + 3 + + + + + True + True + True + + + True + False + go-down-symbolic + + + + + False + True + end + 4 + + + + + True + True + True + + + True + False + go-up-symbolic + + + + + False + True + end + 5 + + + + + False + True + 1 + + + + + 0 + 1 + 2 + + + + + + + True + False + Tracks + + + Save + True + False + True + True + + + + end + + + + + Cancel + True + True + True + + + 1 + + + + + + diff --git a/res/ui/window.ui b/res/ui/window.ui index 68dd11b..f15c310 100644 --- a/res/ui/window.ui +++ b/res/ui/window.ui @@ -193,6 +193,10 @@ Add recording win.add-recording + + Add tracks + win.add-tracks + diff --git a/src/backend.rs b/src/backend.rs index ae1f9af..ba01df3 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -2,6 +2,8 @@ use super::database::*; use anyhow::Result; use futures_channel::oneshot; use futures_channel::oneshot::Sender; +use std::cell::RefCell; +use std::path::PathBuf; enum BackendAction { UpdatePerson(Person, Sender>), @@ -32,10 +34,11 @@ use BackendAction::*; pub struct Backend { action_sender: std::sync::mpsc::Sender, + music_library_path: RefCell>, } impl Backend { - pub fn new(url: &str) -> Self { + pub fn new(url: &str, music_library_path: PathBuf) -> Self { let url = url.to_string(); let (action_sender, action_receiver) = std::sync::mpsc::channel::(); @@ -161,6 +164,7 @@ impl Backend { Backend { action_sender: action_sender, + music_library_path: RefCell::new(Some(music_library_path)), } } @@ -309,4 +313,12 @@ impl Backend { .send(GetRecordingsForWork(work_id, sender))?; receiver.await? } + + pub fn set_music_library_path(&self, path: &str) { + self.music_library_path.replace(Some(PathBuf::from(path))); + } + + pub fn get_music_library_path(&self) -> Option { + self.music_library_path.borrow().clone() + } } diff --git a/src/database/models.rs b/src/database/models.rs index 2c94afc..f9fc117 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -181,3 +181,9 @@ impl From for RecordingInsertion { } } } + +#[derive(Debug, Clone)] +pub struct Track { + pub work_parts: Vec, + pub file_name: String, +} diff --git a/src/dialogs/mod.rs b/src/dialogs/mod.rs index 0a472fe..68c7275 100644 --- a/src/dialogs/mod.rs +++ b/src/dialogs/mod.rs @@ -31,6 +31,12 @@ pub use recording_selector::*; pub mod section_editor; pub use section_editor::*; +pub mod track_editor; +pub use track_editor::*; + +pub mod tracks_editor; +pub use tracks_editor::*; + pub mod work_editor; pub use work_editor::*; diff --git a/src/dialogs/track_editor.rs b/src/dialogs/track_editor.rs new file mode 100644 index 0000000..62816aa --- /dev/null +++ b/src/dialogs/track_editor.rs @@ -0,0 +1,117 @@ +use crate::database::*; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::convert::TryInto; +use std::rc::Rc; + +pub struct TrackEditor { + window: gtk::Window, +} + +impl TrackEditor { + pub fn new(parent: &W, track: Track, work: WorkDescription, callback: F) -> Self + where + W: IsA, + F: Fn(Track) -> () + 'static, + { + let builder = gtk::Builder::from_resource("/de/johrpan/musicus_editor/ui/track_editor.ui"); + + get_widget!(builder, gtk::Window, window); + get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Button, save_button); + get_widget!(builder, gtk::ListBox, list); + + window.set_transient_for(Some(parent)); + + cancel_button.connect_clicked(clone!(@strong window => move |_| { + window.close(); + })); + + 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 work_parts, @strong window => move |_| { + let mut work_parts = work_parts.borrow_mut(); + work_parts.sort(); + + callback(Track { + work_parts: work_parts.clone(), + file_name: file_name.clone(), + }); + + window.close(); + })); + + 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; + } + + Self { window } + } + + pub fn show(&self) { + self.window.show(); + } +} diff --git a/src/dialogs/tracks_editor.rs b/src/dialogs/tracks_editor.rs new file mode 100644 index 0000000..6460375 --- /dev/null +++ b/src/dialogs/tracks_editor.rs @@ -0,0 +1,225 @@ +use super::*; +use crate::backend::*; +use crate::database::*; +use crate::widgets::*; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; + +pub struct TracksEditor { + window: gtk::Window, +} + +impl TracksEditor { + pub fn new () + 'static, P: IsA>( + backend: Rc, + parent: &P, + callback: F, + ) -> Self { + let builder = gtk::Builder::from_resource("/de/johrpan/musicus_editor/ui/tracks_editor.ui"); + + get_widget!(builder, gtk::Window, window); + get_widget!(builder, gtk::Button, cancel_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); + + window.set_transient_for(Some(parent)); + + cancel_button.connect_clicked(clone!(@strong window => move |_| { + window.close(); + })); + + let recording = Rc::new(RefCell::new(None::)); + let tracks = Rc::new(RefCell::new(Vec::::new())); + + let track_list = List::new( + clone!(@strong recording => move |track: &Track| { + let mut title_parts = Vec::::new(); + for part in &track.work_parts { + if let Some(recording) = &*recording.borrow() { + title_parts.push(recording.work.parts[*part].title.clone()); + } + } + + let title = if title_parts.is_empty() { + String::from("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.add(&title_label); + vbox.add(&file_name_label); + + vbox.upcast() + }), + |_| true, + "Add some tracks.", + ); + + let autofill_parts = Rc::new(clone!(@strong recording, @strong tracks, @strong track_list => move || { + if let Some(recording) = &*recording.borrow() { + let mut tracks = 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; + } + } + + track_list.show_items(tracks.clone()); + } + })); + + recording_button.connect_clicked(clone!( + @strong backend, + @strong window, + @strong save_button, + @strong work_label, + @strong performers_label, + @strong recording_stack, + @strong recording, + @strong autofill_parts => move |_| { + RecordingSelector::new( + backend.clone(), + &window, + clone!( + @strong save_button, + @strong work_label, + @strong performers_label, + @strong recording_stack, + @strong recording, + @strong autofill_parts => move |r| { + work_label.set_text(&r.work.get_title()); + performers_label.set_text(&r.get_performers()); + recording_stack.set_visible_child_name("selected"); + recording.replace(Some(r)); + save_button.set_sensitive(true); + autofill_parts(); + } + )).show(); + } + )); + + save_button.connect_clicked(clone!(@strong window => move |_| { + window.close(); + callback(); + })); + + add_track_button.connect_clicked(clone!(@strong window, @strong tracks, @strong track_list, @strong autofill_parts => move |_| { + let music_library_path = backend.get_music_library_path().unwrap(); + + let dialog = gtk::FileChooserNative::new(Some("Select audio files"), Some(&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 track_list.get_selected_index() { + Some(index) => index + 1, + None => tracks.borrow().len(), + }; + + { + let mut tracks = 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; + } + } + + track_list.show_items(tracks.borrow().clone()); + autofill_parts(); + track_list.select_index(index); + } + })); + + remove_track_button.connect_clicked( + clone!(@strong tracks, @strong track_list => move |_| { + match track_list.get_selected_index() { + Some(index) => { + tracks.borrow_mut().remove(index); + track_list.show_items(tracks.borrow().clone()); + track_list.select_index(index); + } + None => (), + } + }), + ); + + move_track_up_button.connect_clicked( + clone!(@strong tracks, @strong track_list => move |_| { + match track_list.get_selected_index() { + Some(index) => { + if index > 0 { + tracks.borrow_mut().swap(index - 1, index); + track_list.show_items(tracks.borrow().clone()); + track_list.select_index(index - 1); + } + } + None => (), + } + }), + ); + + move_track_down_button.connect_clicked( + clone!(@strong tracks, @strong track_list => move |_| { + match track_list.get_selected_index() { + Some(index) => { + if index < tracks.borrow().len() - 1 { + tracks.borrow_mut().swap(index, index + 1); + track_list.show_items(tracks.borrow().clone()); + track_list.select_index(index + 1); + } + } + None => (), + } + }), + ); + + edit_track_button.connect_clicked(clone!(@strong window, @strong tracks, @strong track_list, @strong recording => move |_| { + if let Some(index) = track_list.get_selected_index() { + if let Some(recording) = &*recording.borrow() { + TrackEditor::new(&window, tracks.borrow()[index].clone(), recording.work.clone(), clone!(@strong tracks, @strong track_list => move |track| { + let mut tracks = tracks.borrow_mut(); + tracks[index] = track; + track_list.show_items(tracks.clone()); + track_list.select_index(index); + })).show(); + } + } + })); + + scroll.add(&track_list.widget); + + Self { window } + } + + pub fn show(&self) { + self.window.show(); + } +} diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 01216aa..bed3b72 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -70,6 +70,27 @@ where self.selected.replace(Some(Box::new(selected))); } + pub fn get_selected_index(&self) -> Option { + match self.widget.get_selected_rows().first() { + Some(row) => match row.get_child() { + Some(child) => Some( + child + .downcast::() + .unwrap() + .get_index() + .try_into() + .unwrap(), + ), + None => None, + }, + None => None, + } + } + + pub fn select_index(&self, index: usize) { + self.widget.select_row(self.widget.get_row_at_index(index.try_into().unwrap()).as_ref()); + } + pub fn show_items(&self, items: Vec) { self.items.replace(items); diff --git a/src/window.rs b/src/window.rs index 2a322da..cc50b47 100644 --- a/src/window.rs +++ b/src/window.rs @@ -27,7 +27,7 @@ impl Window { get_widget!(builder, gtk::Box, sidebar_box); get_widget!(builder, gtk::Box, empty_screen); - let backend = Rc::new(Backend::new("test.sqlite")); + let backend = Rc::new(Backend::new("test.sqlite", std::env::current_dir().unwrap())); let poe_list = PoeList::new(backend.clone()); let navigator = Navigator::new(&empty_screen); @@ -110,6 +110,16 @@ impl Window { }) ); + action!( + result.window, + "add-tracks", + clone!(@strong result => move |_, _| { + TracksEditor::new(result.backend.clone(), &result.window, clone!(@strong result => move || { + result.reload(); + })).show(); + }) + ); + action!( result.window, "edit-person",