From d7401195b32bfebf88cab50a167c9a9f777800a0 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Fri, 17 Jan 2025 17:54:16 +0100 Subject: [PATCH] editor: Functional recording and work editor --- data/ui/work_editor_part_row.blp | 21 +++ src/db/models.rs | 53 ++------ src/editor/mod.rs | 1 + src/editor/recording_editor.rs | 53 ++++++-- src/editor/work_editor.rs | 160 ++++++++++++++-------- src/editor/work_editor_part_row.rs | 129 ++++++++++++++++++ src/home_page.rs | 1 + src/library.rs | 207 +++++++++++++++++++++++++---- 8 files changed, 487 insertions(+), 138 deletions(-) create mode 100644 data/ui/work_editor_part_row.blp create mode 100644 src/editor/work_editor_part_row.rs diff --git a/data/ui/work_editor_part_row.blp b/data/ui/work_editor_part_row.blp new file mode 100644 index 0000000..dbbbb8e --- /dev/null +++ b/data/ui/work_editor_part_row.blp @@ -0,0 +1,21 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusWorkEditorPartRow: Adw.ActionRow { + activatable: true; + activated => $edit() swapped; + + Gtk.Image { + icon-name: "document-edit-symbolic"; + } + + Gtk.Button { + icon-name: "user-trash-symbolic"; + valign: center; + clicked => $remove() swapped; + + styles [ + "flat" + ] + } +} diff --git a/src/db/models.rs b/src/db/models.rs index 54b0a78..060e58d 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -17,19 +17,11 @@ pub use tables::{Album, Instrument, Person, Role}; pub struct Work { pub work_id: String, pub name: TranslatedString, - pub parts: Vec, + pub parts: Vec, pub persons: Vec, pub instruments: Vec, } -// TODO: Handle part composers. -#[derive(Default, Clone, Debug)] -pub struct WorkPart { - pub work_id: String, - pub level: u8, - pub name: TranslatedString, -} - #[derive(Queryable, Selectable, Clone, Debug)] pub struct Composer { #[diesel(embed)] @@ -122,42 +114,17 @@ impl PartialEq for Composer { } } -impl Eq for WorkPart {} -impl PartialEq for WorkPart { - fn eq(&self, other: &Self) -> bool { - self.work_id == other.work_id - } -} - impl Work { pub fn from_table(data: tables::Work, connection: &mut SqliteConnection) -> Result { - fn visit_children( - work_id: &str, - level: u8, - connection: &mut SqliteConnection, - ) -> Result> { - let mut parts = Vec::new(); - - let children: Vec = works::table - .filter(works::parent_work_id.eq(work_id)) - .load(connection)?; - - for child in children { - let mut grand_children = visit_children(&child.work_id, level + 1, connection)?; - - parts.push(WorkPart { - work_id: child.work_id, - level, - name: child.name, - }); - - parts.append(&mut grand_children); - } - - Ok(parts) - } - - let parts = visit_children(&data.work_id, 0, connection)?; + // Note: Because this calls Work::from_table for each part, this recursively + // adds all children. It does not check for circularity. + let parts = works::table + .order(works::sequence_number) + .filter(works::parent_work_id.eq(&data.work_id)) + .load::(connection)? + .into_iter() + .map(|w| Work::from_table(w, connection)) + .collect::>>()?; let persons: Vec = persons::table .inner_join(work_persons::table.inner_join(roles::table)) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 8eb1c32..11deb59 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -15,4 +15,5 @@ pub mod translation_editor; pub mod translation_entry; pub mod work_editor; pub mod work_editor_composer_row; +pub mod work_editor_part_row; pub mod work_selector_popover; diff --git a/src/editor/recording_editor.rs b/src/editor/recording_editor.rs index 75488b6..f4afe85 100644 --- a/src/editor/recording_editor.rs +++ b/src/editor/recording_editor.rs @@ -102,7 +102,7 @@ mod imp { let obj = self.obj().clone(); work_selector_popover.connect_create(move |_| { - let editor = MusicusWorkEditor::new(&obj.navigation(), &obj.library(), None); + let editor = MusicusWorkEditor::new(&obj.navigation(), &obj.library(), None, false); editor.connect_created(clone!( #[weak] @@ -124,7 +124,7 @@ mod imp { let obj = self.obj().clone(); persons_popover.connect_person_selected(move |_, person| { - obj.add_performer(person); + obj.new_performer(person); }); let obj = self.obj().clone(); @@ -135,7 +135,7 @@ mod imp { #[weak] obj, move |_, person| { - obj.add_performer(person); + obj.new_performer(person); } )); @@ -150,7 +150,7 @@ mod imp { let obj = self.obj().clone(); ensembles_popover.connect_ensemble_selected(move |_, ensemble| { - obj.add_ensemble(ensemble); + obj.new_ensemble_performer(ensemble); }); let obj = self.obj().clone(); @@ -161,7 +161,7 @@ mod imp { #[weak] obj, move |_, ensemble| { - obj.add_ensemble(ensemble); + obj.new_ensemble_performer(ensemble); } )); @@ -200,7 +200,20 @@ impl MusicusRecordingEditor { .recording_id .set(recording.recording_id.clone()) .unwrap(); - // TODO: Initialize data. + + obj.set_work(recording.work.clone()); + + if let Some(year) = recording.year { + obj.imp().year_row.set_value(year as f64); + } + + for performer in recording.persons.clone() { + obj.add_performer_row(performer); + } + + for ensemble_performer in recording.ensembles.clone() { + obj.add_ensemble_row(ensemble_performer); + } } obj @@ -227,14 +240,17 @@ impl MusicusRecordingEditor { self.imp().work.replace(Some(work)); } - fn add_performer(&self, person: Person) { - let role = self.library().performer_default_role().unwrap(); + fn new_performer(&self, person: Person) { let performer = Performer { person, - role, + role: self.library().performer_default_role().unwrap(), instrument: None, }; + self.add_performer_row(performer); + } + + fn add_performer_row(&self, performer: Performer) { let row = MusicusRecordingEditorPerformerRow::new(&self.navigation(), &self.library(), performer); @@ -254,12 +270,21 @@ impl MusicusRecordingEditor { self.imp().performer_rows.borrow_mut().push(row); } - fn add_ensemble(&self, ensemble: Ensemble) { - let role = self.library().performer_default_role().unwrap(); - let performer = EnsemblePerformer { ensemble, role }; + fn new_ensemble_performer(&self, ensemble: Ensemble) { + let performer = EnsemblePerformer { + ensemble, + role: self.library().performer_default_role().unwrap(), + }; - let row = - MusicusRecordingEditorEnsembleRow::new(&self.navigation(), &self.library(), performer); + self.add_ensemble_row(performer); + } + + fn add_ensemble_row(&self, ensemble_performer: EnsemblePerformer) { + let row = MusicusRecordingEditorEnsembleRow::new( + &self.navigation(), + &self.library(), + ensemble_performer, + ); row.connect_remove(clone!( #[weak(rename_to = this)] diff --git a/src/editor/work_editor.rs b/src/editor/work_editor.rs index 915dcd3..14ddda3 100644 --- a/src/editor/work_editor.rs +++ b/src/editor/work_editor.rs @@ -1,4 +1,18 @@ -use std::cell::{OnceCell, RefCell}; +use crate::{ + db::{ + self, + models::{Composer, Instrument, Person, Work}, + }, + editor::{ + instrument_editor::MusicusInstrumentEditor, + instrument_selector_popover::MusicusInstrumentSelectorPopover, + person_editor::MusicusPersonEditor, person_selector_popover::MusicusPersonSelectorPopover, + translation_editor::MusicusTranslationEditor, + work_editor_composer_row::MusicusWorkEditorComposerRow, + work_editor_part_row::MusicusWorkEditorPartRow, + }, + library::MusicusLibrary, +}; use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; @@ -8,20 +22,7 @@ use gtk::glib::{ }; use once_cell::sync::Lazy; -use crate::{ - db::{ - self, - models::{Composer, Instrument, Person, Work, WorkPart}, - }, - editor::{ - instrument_editor::MusicusInstrumentEditor, - instrument_selector_popover::MusicusInstrumentSelectorPopover, - person_editor::MusicusPersonEditor, person_selector_popover::MusicusPersonSelectorPopover, - translation_editor::MusicusTranslationEditor, - work_editor_composer_row::MusicusWorkEditorComposerRow, - }, - library::MusicusLibrary, -}; +use std::cell::{Cell, OnceCell, RefCell}; mod imp { use super::*; @@ -37,13 +38,14 @@ mod imp { pub library: OnceCell, pub work_id: OnceCell, + pub is_part_editor: Cell, // Holding a reference to each composer row is the simplest way to enumerate all // results when finishing the process of editing the work. The composer rows // handle all state related to the composer. pub composer_rows: RefCell>, - // TODO: These need to be PartRows! - pub parts: RefCell>, + pub part_rows: RefCell>, + pub instruments: RefCell>, pub persons_popover: OnceCell, @@ -165,16 +167,36 @@ impl MusicusWorkEditor { navigation: &adw::NavigationView, library: &MusicusLibrary, work: Option<&Work>, + is_part_editor: bool, ) -> Self { let obj: Self = glib::Object::builder() .property("navigation", navigation) .property("library", library) .build(); + if is_part_editor { + obj.set_title(&gettext("Work part")); + obj.imp().save_button.set_label(&gettext("Add work part")); + obj.imp().is_part_editor.set(true); + } + if let Some(work) = work { obj.imp().save_button.set_label(&gettext("Save changes")); obj.imp().work_id.set(work.work_id.clone()).unwrap(); - // TODO: Initialize work data. + + obj.imp().name_editor.set_translation(&work.name); + + for part in &work.parts { + obj.add_part_row(part.clone()); + } + + for composer in &work.persons { + obj.add_composer_row(composer.clone()); + } + + for instrument in &work.instruments { + obj.add_instrument_row(instrument.clone()); + } } obj @@ -196,41 +218,17 @@ impl MusicusWorkEditor { #[template_callback] fn add_part(&self, _: &adw::ActionRow) { - let part = WorkPart { - work_id: db::generate_id(), - ..Default::default() - }; + let editor = MusicusWorkEditor::new(&self.navigation(), &self.library(), None, true); - let row = adw::EntryRow::builder().title(gettext("Name")).build(); - - let remove_button = gtk::Button::builder() - .icon_name("user-trash-symbolic") - .valign(gtk::Align::Center) - .css_classes(["flat"]) - .build(); - - remove_button.connect_clicked(clone!( + editor.connect_created(clone!( #[weak(rename_to = this)] self, - #[weak] - row, - #[strong] - part, - move |_| { - this.imp().part_list.remove(&row); - this.imp().parts.borrow_mut().retain(|p| *p != part); + move |_, part| { + this.add_part_row(part); } )); - row.add_suffix(&remove_button); - - self.imp() - .part_list - .insert(&row, self.imp().parts.borrow().len() as i32); - - row.grab_focus(); - - self.imp().parts.borrow_mut().push(part); + self.navigation().push(&editor); } #[template_callback] @@ -241,6 +239,29 @@ impl MusicusWorkEditor { fn add_composer(&self, person: Person) { let role = self.library().composer_default_role().unwrap(); let composer = Composer { person, role }; + self.add_composer_row(composer); + } + + fn add_part_row(&self, part: Work) { + let row = MusicusWorkEditorPartRow::new(&self.navigation(), &self.library(), part); + + row.connect_remove(clone!( + #[weak(rename_to = this)] + self, + move |row| { + this.imp().part_list.remove(row); + this.imp().part_rows.borrow_mut().retain(|p| p != row); + } + )); + + self.imp() + .part_list + .insert(&row, self.imp().part_rows.borrow().len() as i32); + + self.imp().part_rows.borrow_mut().push(row); + } + + fn add_composer_row(&self, composer: Composer) { let row = MusicusWorkEditorComposerRow::new(&self.navigation(), &self.library(), composer); row.connect_remove(clone!( @@ -300,7 +321,15 @@ impl MusicusWorkEditor { let library = self.imp().library.get().unwrap(); let name = self.imp().name_editor.translation(); - let parts = self.imp().parts.borrow().clone(); + + let parts = self + .imp() + .part_rows + .borrow() + .iter() + .map(|p| p.part()) + .collect::>(); + let composers = self .imp() .composer_rows @@ -310,15 +339,34 @@ impl MusicusWorkEditor { .collect::>(); let instruments = self.imp().instruments.borrow().clone(); - if let Some(work_id) = self.imp().work_id.get() { - library - .update_work(work_id, name, parts, composers, instruments) - .unwrap(); + if self.imp().is_part_editor.get() { + let work_id = self + .imp() + .work_id + .get() + .map(|w| w.to_string()) + .unwrap_or_else(db::generate_id); + + let part = Work { + work_id, + name, + parts, + persons: composers, + instruments, + }; + + self.emit_by_name::<()>("created", &[&part]); } else { - let work = library - .create_work(name, parts, composers, instruments) - .unwrap(); - self.emit_by_name::<()>("created", &[&work]); + if let Some(work_id) = self.imp().work_id.get() { + library + .update_work(work_id, name, parts, composers, instruments) + .unwrap(); + } else { + let work = library + .create_work(name, parts, composers, instruments) + .unwrap(); + self.emit_by_name::<()>("created", &[&work]); + } } self.imp().navigation.get().unwrap().pop(); diff --git a/src/editor/work_editor_part_row.rs b/src/editor/work_editor_part_row.rs new file mode 100644 index 0000000..5c8a3f8 --- /dev/null +++ b/src/editor/work_editor_part_row.rs @@ -0,0 +1,129 @@ +use crate::{db::models::Work, editor::work_editor::MusicusWorkEditor, library::MusicusLibrary}; + +use adw::{prelude::*, subclass::prelude::*}; +use gtk::glib::{self, clone, subclass::Signal, Properties}; +use once_cell::sync::Lazy; + +use std::cell::{OnceCell, RefCell}; + +mod imp { + + use super::*; + + #[derive(Properties, Debug, Default, gtk::CompositeTemplate)] + #[properties(wrapper_type = super::MusicusWorkEditorPartRow)] + #[template(file = "data/ui/work_editor_part_row.blp")] + pub struct MusicusWorkEditorPartRow { + #[property(get, construct_only)] + pub navigation: OnceCell, + + #[property(get, construct_only)] + pub library: OnceCell, + + pub part: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for MusicusWorkEditorPartRow { + const NAME: &'static str = "MusicusWorkEditorPartRow"; + type Type = super::MusicusWorkEditorPartRow; + type ParentType = adw::ActionRow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for MusicusWorkEditorPartRow { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("remove").build()]); + + SIGNALS.as_ref() + } + } + + impl WidgetImpl for MusicusWorkEditorPartRow {} + impl ListBoxRowImpl for MusicusWorkEditorPartRow {} + impl PreferencesRowImpl for MusicusWorkEditorPartRow {} + impl ActionRowImpl for MusicusWorkEditorPartRow {} +} + +glib::wrapper! { + pub struct MusicusWorkEditorPartRow(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow; +} + +#[gtk::template_callbacks] +impl MusicusWorkEditorPartRow { + pub fn new(navigation: &adw::NavigationView, library: &MusicusLibrary, part: Work) -> Self { + let obj: Self = glib::Object::builder() + .property("navigation", navigation) + .property("library", library) + .build(); + obj.set_part(part); + obj + } + + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("remove", true, move |values| { + let obj = values[0].get::().unwrap(); + f(&obj); + None + }) + } + + pub fn part(&self) -> Work { + self.imp().part.borrow().to_owned().unwrap() + } + + fn set_part(&self, part: Work) { + self.set_title(&part.name.get()); + + if !part.parts.is_empty() { + self.set_subtitle( + &part + .parts + .iter() + .map(|p| p.name.get()) + .collect::>() + .join("\n"), + ); + } else { + self.set_subtitle(""); + } + + self.imp().part.replace(Some(part)); + } + + #[template_callback] + fn edit(&self) { + let editor = MusicusWorkEditor::new( + &self.navigation(), + &self.library(), + self.imp().part.borrow().as_ref(), + true, + ); + + editor.connect_created(clone!( + #[weak(rename_to = this)] + self, + move |_, part| { + this.set_part(part); + } + )); + + self.navigation().push(&editor); + } + + #[template_callback] + fn remove(&self, _: >k::Button) { + self.emit_by_name::<()>("remove", &[]); + } +} diff --git a/src/home_page.rs b/src/home_page.rs index 9487152..0a1bb73 100644 --- a/src/home_page.rs +++ b/src/home_page.rs @@ -173,6 +173,7 @@ impl MusicusHomePage { &self.navigation(), &self.library(), Some(work), + false, )), } } diff --git a/src/library.rs b/src/library.rs index 17c5252..5a75995 100644 --- a/src/library.rs +++ b/src/library.rs @@ -848,20 +848,33 @@ impl MusicusLibrary { pub fn create_work( &self, name: TranslatedString, - parts: Vec, + parts: Vec, persons: Vec, instruments: Vec, ) -> Result { let mut binding = self.imp().connection.borrow_mut(); let connection = &mut *binding.as_mut().unwrap(); + self.create_work_priv(connection, name, parts, persons, instruments, None, None) + } + + fn create_work_priv( + &self, + connection: &mut SqliteConnection, + name: TranslatedString, + parts: Vec, + persons: Vec, + instruments: Vec, + parent_work_id: Option<&str>, + sequence_number: Option, + ) -> Result { let work_id = db::generate_id(); let now = Local::now().naive_local(); let work_data = tables::Work { work_id: work_id.clone(), - parent_work_id: None, - sequence_number: None, + parent_work_id: parent_work_id.map(|w| w.to_string()), + sequence_number: sequence_number, name, created_at: now, edited_at: now, @@ -874,20 +887,15 @@ impl MusicusLibrary { .execute(connection)?; for (index, part) in parts.into_iter().enumerate() { - let part_data = tables::Work { - work_id: part.work_id, - parent_work_id: Some(work_id.clone()), - sequence_number: Some(index as i32), - name: part.name, - created_at: now, - edited_at: now, - last_used_at: now, - last_played_at: None, - }; - - diesel::insert_into(works::table) - .values(&part_data) - .execute(connection)?; + self.create_work_priv( + connection, + part.name, + part.parts, + part.persons, + part.instruments, + Some(&work_id), + Some(index as i32), + )?; } for (index, composer) in persons.into_iter().enumerate() { @@ -922,20 +930,125 @@ impl MusicusLibrary { pub fn update_work( &self, - id: &str, + work_id: &str, name: TranslatedString, - parts: Vec, + parts: Vec, persons: Vec, instruments: Vec, ) -> Result<()> { let mut binding = self.imp().connection.borrow_mut(); let connection = &mut *binding.as_mut().unwrap(); + self.update_work_priv( + connection, + work_id, + name, + parts, + persons, + instruments, + None, + None, + ) + } + + fn update_work_priv( + &self, + connection: &mut SqliteConnection, + work_id: &str, + name: TranslatedString, + parts: Vec, + persons: Vec, + instruments: Vec, + parent_work_id: Option<&str>, + sequence_number: Option, + ) -> Result<()> { let now = Local::now().naive_local(); - // TODO: Update work, check which work parts etc exist, update them, - // create new work parts, delete and readd composers and instruments. - todo!() + diesel::update(works::table) + .filter(works::work_id.eq(work_id)) + .set(( + works::parent_work_id.eq(parent_work_id), + works::sequence_number.eq(sequence_number), + works::name.eq(name), + works::edited_at.eq(now), + works::last_used_at.eq(now), + )) + .execute(connection)?; + + diesel::delete(works::table) + .filter( + works::parent_work_id + .eq(work_id) + .and(works::work_id.ne_all(parts.iter().map(|p| p.work_id.clone()))), + ) + .execute(connection)?; + + for (index, part) in parts.into_iter().enumerate() { + if works::table + .filter(works::work_id.eq(&part.work_id)) + .first::(connection) + .optional()? + .is_some() + { + self.update_work_priv( + connection, + &part.work_id, + part.name, + part.parts, + part.persons, + part.instruments, + Some(work_id), + Some(index as i32), + )?; + } else { + // Note: The previously used ID is discarded. This should be OK, because + // at this point, the part ID should not have been used anywhere. + self.create_work_priv( + connection, + part.name, + part.parts, + part.persons, + part.instruments, + Some(work_id), + Some(index as i32), + )?; + } + } + + diesel::delete(work_persons::table) + .filter(work_persons::work_id.eq(work_id)) + .execute(connection)?; + + for (index, composer) in persons.into_iter().enumerate() { + let composer_data = tables::WorkPerson { + work_id: work_id.to_string(), + person_id: composer.person.person_id, + role_id: composer.role.role_id, + sequence_number: index as i32, + }; + + diesel::insert_into(work_persons::table) + .values(composer_data) + .execute(connection)?; + } + + diesel::delete(work_instruments::table) + .filter(work_instruments::work_id.eq(work_id)) + .execute(connection)?; + + for (index, instrument) in instruments.into_iter().enumerate() { + let instrument_data = tables::WorkInstrument { + work_id: work_id.to_string(), + instrument_id: instrument.instrument_id, + sequence_number: index as i32, + }; + + diesel::insert_into(work_instruments::table) + .values(instrument_data) + .execute(connection)?; + } + + Ok(()) } pub fn create_ensemble(&self, name: TranslatedString) -> Result { @@ -1045,7 +1158,7 @@ impl MusicusLibrary { pub fn update_recording( &self, - id: &str, + recording_id: &str, work: Work, year: Option, performers: Vec, @@ -1056,8 +1169,52 @@ impl MusicusLibrary { let now = Local::now().naive_local(); - // TODO: Update recording. - todo!() + diesel::update(recordings::table) + .filter(recordings::recording_id.eq(recording_id)) + .set(( + recordings::work_id.eq(work.work_id), + recordings::year.eq(year), + recordings::edited_at.eq(now), + recordings::last_used_at.eq(now), + )) + .execute(connection)?; + + diesel::delete(recording_persons::table) + .filter(recording_persons::recording_id.eq(recording_id)) + .execute(connection)?; + + for (index, performer) in performers.into_iter().enumerate() { + let recording_person_data = tables::RecordingPerson { + recording_id: recording_id.to_string(), + person_id: performer.person.person_id, + role_id: performer.role.role_id, + instrument_id: performer.instrument.map(|i| i.instrument_id), + sequence_number: index as i32, + }; + + diesel::insert_into(recording_persons::table) + .values(&recording_person_data) + .execute(connection)?; + } + + diesel::delete(recording_ensembles::table) + .filter(recording_ensembles::recording_id.eq(recording_id)) + .execute(connection)?; + + for (index, ensemble) in ensembles.into_iter().enumerate() { + let recording_ensemble_data = tables::RecordingEnsemble { + recording_id: recording_id.to_string(), + ensemble_id: ensemble.ensemble.ensemble_id, + role_id: ensemble.role.role_id, + sequence_number: index as i32, + }; + + diesel::insert_into(recording_ensembles::table) + .values(&recording_ensemble_data) + .execute(connection)?; + } + + Ok(()) } }