Fully functional person editor

This commit is contained in:
Elias Projahn 2024-06-06 15:17:56 +02:00
parent f49f23a501
commit 3dc601e0f0
10 changed files with 214 additions and 43 deletions

View file

@ -11,7 +11,7 @@
border-radius: 100px; border-radius: 100px;
} }
.searchbar .searchtag > button { .searchbar .searchtag>button {
min-width: 24px; min-width: 24px;
min-height: 24px; min-height: 24px;
margin: 0px; margin: 0px;
@ -22,7 +22,7 @@
min-width: 200px; min-width: 200px;
} }
.tile > box { .tile>box {
margin: 6px 12px; margin: 6px 12px;
} }
@ -48,7 +48,7 @@
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
} }
.playlist > row { .playlist>row {
border-radius: 12px; border-radius: 12px;
} }
@ -64,7 +64,7 @@
font-size: smaller; font-size: smaller;
} }
.selector > contents { .selector>contents {
padding: 0; padding: 0;
} }
@ -74,7 +74,14 @@
padding-bottom: 8px; padding-bottom: 8px;
} }
.selector-list > row { .selector-list>row {
padding: 6px; padding: 6px;
border-radius: 6px; border-radius: 6px;
} }
button.save {
min-height: 48px;
font-weight: 700;
font-size: 11pt;
color: var(--accent-color);
}

View file

@ -25,6 +25,17 @@ template $MusicusPersonEditor: Adw.NavigationPage {
$MusicusTranslationEditor name_editor { $MusicusTranslationEditor name_editor {
margin-top: 12; margin-top: 12;
} }
Gtk.Button save_button {
margin-top: 24;
label: _("Create person");
clicked => $save() swapped;
styles [
"card",
"save"
]
}
} }
} }
} }

View file

@ -67,9 +67,12 @@ mod imp {
fn signals() -> &'static [Signal] { fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| { static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("instrument-selected") vec![
Signal::builder("instrument-selected")
.param_types([Instrument::static_type()]) .param_types([Instrument::static_type()])
.build()] .build(),
Signal::builder("create").build(),
]
}); });
SIGNALS.as_ref() SIGNALS.as_ref()
@ -113,6 +116,14 @@ impl MusicusInstrumentSelectorPopover {
}) })
} }
pub fn connect_create<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("create", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
#[template_callback] #[template_callback]
fn search_changed(&self, entry: &gtk::SearchEntry) { fn search_changed(&self, entry: &gtk::SearchEntry) {
self.search(&entry.text()); self.search(&entry.text());
@ -135,7 +146,12 @@ impl MusicusInstrumentSelectorPopover {
fn search(&self, search: &str) { fn search(&self, search: &str) {
let imp = self.imp(); let imp = self.imp();
let instruments = imp.library.get().unwrap().search_instruments(search).unwrap(); let instruments = imp
.library
.get()
.unwrap()
.search_instruments(search)
.unwrap();
imp.list_box.remove_all(); imp.list_box.remove_all();
@ -182,7 +198,7 @@ impl MusicusInstrumentSelectorPopover {
} }
fn create(&self) { fn create(&self) {
log::info!("Create instrument!"); self.emit_by_name::<()>("create", &[]);
self.popdown(); self.popdown();
} }
} }

View file

@ -1,16 +1,30 @@
use adw::{prelude::*, subclass::prelude::*}; use std::cell::OnceCell;
use gtk::glib;
use crate::{db::models::Person, editor::translation_editor::MusicusTranslationEditor}; use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::glib::{self, subclass::Signal};
use once_cell::sync::Lazy;
use crate::{
db::models::Person, editor::translation_editor::MusicusTranslationEditor,
library::MusicusLibrary,
};
mod imp { mod imp {
use super::*; use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)] #[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/person_editor.blp")] #[template(file = "data/ui/person_editor.blp")]
pub struct MusicusPersonEditor { pub struct MusicusPersonEditor {
pub navigation: OnceCell<adw::NavigationView>,
pub library: OnceCell<MusicusLibrary>,
pub person_id: OnceCell<String>,
#[template_child] #[template_child]
pub name_editor: TemplateChild<MusicusTranslationEditor>, pub name_editor: TemplateChild<MusicusTranslationEditor>,
#[template_child]
pub save_button: TemplateChild<gtk::Button>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -30,7 +44,18 @@ mod imp {
} }
} }
impl ObjectImpl for MusicusPersonEditor {} impl ObjectImpl for MusicusPersonEditor {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("created")
.param_types([Person::static_type()])
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusPersonEditor {} impl WidgetImpl for MusicusPersonEditor {}
impl NavigationPageImpl for MusicusPersonEditor {} impl NavigationPageImpl for MusicusPersonEditor {}
} }
@ -42,13 +67,46 @@ glib::wrapper! {
#[gtk::template_callbacks] #[gtk::template_callbacks]
impl MusicusPersonEditor { impl MusicusPersonEditor {
pub fn new(person: Option<&Person>) -> Self { pub fn new(
navigation: &adw::NavigationView,
library: &MusicusLibrary,
person: Option<&Person>,
) -> Self {
let obj: Self = glib::Object::new(); let obj: Self = glib::Object::new();
obj.imp().navigation.set(navigation.to_owned()).unwrap();
obj.imp().library.set(library.to_owned()).unwrap();
if let Some(person) = person { if let Some(person) = person {
obj.imp().save_button.set_label(&gettext("Save changes"));
obj.imp().person_id.set(person.person_id.clone()).unwrap();
obj.imp().name_editor.set_translation(&person.name); obj.imp().name_editor.set_translation(&person.name);
} }
obj obj
} }
pub fn connect_created<F: Fn(&Self, Person) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("created", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let person = values[1].get::<Person>().unwrap();
f(&obj, person);
None
})
}
#[template_callback]
fn save(&self, _: &gtk::Button) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
if let Some(person_id) = self.imp().person_id.get() {
library.update_person(person_id, name).unwrap();
} else {
let person = library.create_person(name).unwrap();
self.emit_by_name::<()>("created", &[&person]);
}
self.imp().navigation.get().unwrap().pop();
}
} }

View file

@ -67,9 +67,12 @@ mod imp {
fn signals() -> &'static [Signal] { fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| { static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("person-selected") vec![
Signal::builder("person-selected")
.param_types([Person::static_type()]) .param_types([Person::static_type()])
.build()] .build(),
Signal::builder("create").build(),
]
}); });
SIGNALS.as_ref() SIGNALS.as_ref()
@ -113,6 +116,14 @@ impl MusicusPersonSelectorPopover {
}) })
} }
pub fn connect_create<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("create", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
#[template_callback] #[template_callback]
fn search_changed(&self, entry: &gtk::SearchEntry) { fn search_changed(&self, entry: &gtk::SearchEntry) {
self.search(&entry.text()); self.search(&entry.text());
@ -182,7 +193,7 @@ impl MusicusPersonSelectorPopover {
} }
fn create(&self) { fn create(&self) {
log::info!("Create person!"); self.emit_by_name::<()>("create", &[]);
self.popdown(); self.popdown();
} }
} }

View file

@ -67,9 +67,12 @@ mod imp {
fn signals() -> &'static [Signal] { fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| { static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("role-selected") vec![
Signal::builder("role-selected")
.param_types([Role::static_type()]) .param_types([Role::static_type()])
.build()] .build(),
Signal::builder("create").build(),
]
}); });
SIGNALS.as_ref() SIGNALS.as_ref()
@ -113,6 +116,14 @@ impl MusicusRoleSelectorPopover {
}) })
} }
pub fn connect_create<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("create", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
#[template_callback] #[template_callback]
fn search_changed(&self, entry: &gtk::SearchEntry) { fn search_changed(&self, entry: &gtk::SearchEntry) {
self.search(&entry.text()); self.search(&entry.text());
@ -182,7 +193,7 @@ impl MusicusRoleSelectorPopover {
} }
fn create(&self) { fn create(&self) {
log::info!("Create role!"); self.emit_by_name::<()>("create", &[]);
self.popdown(); self.popdown();
} }
} }

View file

@ -76,7 +76,7 @@ impl MusicusTranslationEditor {
self.add_entry(&util::LANG, &self.imp().entry_row.text()); self.add_entry(&util::LANG, &self.imp().entry_row.text());
} }
fn translation(&self) -> TranslatedString { pub fn translation(&self) -> TranslatedString {
let imp = self.imp(); let imp = self.imp();
let mut translation = HashMap::<String, String>::new(); let mut translation = HashMap::<String, String>::new();

View file

@ -5,7 +5,7 @@ use crate::{
}, },
editor::{ editor::{
instrument_selector_popover::MusicusInstrumentSelectorPopover, instrument_selector_popover::MusicusInstrumentSelectorPopover,
person_selector_popover::MusicusPersonSelectorPopover, person_editor::MusicusPersonEditor, person_selector_popover::MusicusPersonSelectorPopover,
translation_editor::MusicusTranslationEditor, translation_editor::MusicusTranslationEditor,
work_editor_composer_row::MusicusWorkEditorComposerRow, work_editor_composer_row::MusicusWorkEditorComposerRow,
}, },
@ -80,24 +80,20 @@ mod imp {
let persons_popover = MusicusPersonSelectorPopover::new(self.library.get().unwrap()); let persons_popover = MusicusPersonSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().clone(); let obj = self.obj().clone();
persons_popover.connect_person_selected( persons_popover.connect_person_selected(move |_, person| {
move |_: &MusicusPersonSelectorPopover, person: Person| { obj.add_composer(person);
let role = obj.library().composer_default_role().unwrap(); });
let composer = Composer { person, role };
let row = MusicusWorkEditorComposerRow::new(&obj.library(), composer);
row.connect_remove(clone!(@weak obj => move |row| { let obj = self.obj().clone();
obj.imp().composer_list.remove(row); persons_popover.connect_create(move |_| {
obj.imp().composer_rows.borrow_mut().retain(|c| c != row); let editor = MusicusPersonEditor::new(&obj.navigation(), &obj.library(), None);
editor.connect_created(clone!(@weak obj => move |_, person| {
obj.add_composer(person);
})); }));
obj.imp() obj.navigation().push(&editor);
.composer_list });
.insert(&row, obj.imp().composer_rows.borrow().len() as i32);
obj.imp().composer_rows.borrow_mut().push(row);
},
);
self.select_person_box.append(&persons_popover); self.select_person_box.append(&persons_popover);
self.persons_popover.set(persons_popover).unwrap(); self.persons_popover.set(persons_popover).unwrap();
@ -210,4 +206,21 @@ impl MusicusWorkEditor {
fn add_instrument(&self, _: &adw::ActionRow) { fn add_instrument(&self, _: &adw::ActionRow) {
self.imp().instruments_popover.get().unwrap().popup(); self.imp().instruments_popover.get().unwrap().popup();
} }
fn add_composer(&self, person: Person) {
let role = self.library().composer_default_role().unwrap();
let composer = Composer { person, role };
let row = MusicusWorkEditorComposerRow::new(&self.library(), composer);
row.connect_remove(clone!(@weak self as obj => move |row| {
obj.imp().composer_list.remove(row);
obj.imp().composer_rows.borrow_mut().retain(|c| c != row);
}));
self.imp()
.composer_list
.insert(&row, self.imp().composer_rows.borrow().len() as i32);
self.imp().composer_rows.borrow_mut().push(row);
}
} }

View file

@ -141,8 +141,11 @@ impl MusicusHomePage {
if let Some(tag) = self.imp().search_entry.tags().first() { if let Some(tag) = self.imp().search_entry.tags().first() {
match tag { match tag {
Tag::Composer(person) | Tag::Performer(person) => { Tag::Composer(person) | Tag::Performer(person) => {
self.navigation() self.navigation().push(&MusicusPersonEditor::new(
.push(&MusicusPersonEditor::new(Some(person))); &self.navigation(),
&self.library(),
Some(person),
));
} }
Tag::Ensemble(_) => todo!(), Tag::Ensemble(_) => todo!(),
Tag::Work(work) => self.navigation().push(&MusicusWorkEditor::new( Tag::Work(work) => self.navigation().push(&MusicusWorkEditor::new(

View file

@ -4,10 +4,11 @@ use std::{
}; };
use anyhow::Result; use anyhow::Result;
use chrono::prelude::*;
use diesel::{dsl::exists, prelude::*, QueryDsl, SqliteConnection}; use diesel::{dsl::exists, prelude::*, QueryDsl, SqliteConnection};
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*}; use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use crate::db::{self, models::*, schema::*, tables}; use crate::db::{self, models::*, schema::*, tables, TranslatedString};
diesel::sql_function! { diesel::sql_function! {
/// Represents the SQL RANDOM() function. /// Represents the SQL RANDOM() function.
@ -444,6 +445,46 @@ impl MusicusLibrary {
.filter(roles::role_id.eq("380d7e09eb2f49c1a90db2ba4acb6ffd")) .filter(roles::role_id.eq("380d7e09eb2f49c1a90db2ba4acb6ffd"))
.first::<Role>(connection)?) .first::<Role>(connection)?)
} }
pub fn create_person(&self, name: TranslatedString) -> Result<Person> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let now = Local::now().naive_local();
let person = Person {
person_id: db::generate_id(),
name,
created_at: now,
edited_at: now,
last_used_at: now,
last_played_at: None,
};
diesel::insert_into(persons::table)
.values(&person)
.execute(connection)?;
Ok(person)
}
pub fn update_person(&self, id: &str, name: TranslatedString) -> Result<()> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let now = Local::now().naive_local();
diesel::update(persons::table)
.filter(persons::person_id.eq(id))
.set((
persons::name.eq(name),
persons::edited_at.eq(now),
persons::last_used_at.eq(now),
))
.execute(connection)?;
Ok(())
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]