mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Add work and recording editor
This commit is contained in:
parent
36b2f1097e
commit
364557d959
30 changed files with 3308 additions and 418 deletions
|
|
@ -1,17 +1,19 @@
|
|||
//! This module contains higher-level models combining information from
|
||||
//! multiple database tables.
|
||||
|
||||
use std::{fmt::Display, path::Path};
|
||||
use std::fmt::Display;
|
||||
|
||||
use anyhow::Result;
|
||||
use diesel::prelude::*;
|
||||
use gtk::glib::{self, Boxed};
|
||||
|
||||
use super::{schema::*, tables, TranslatedString};
|
||||
|
||||
// Re-exports for tables that don't need additional information.
|
||||
pub use tables::{Album, Instrument, Person, Role};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Boxed, Clone, Debug)]
|
||||
#[boxed_type(name = "MusicusWork")]
|
||||
pub struct Work {
|
||||
pub work_id: String,
|
||||
pub name: TranslatedString,
|
||||
|
|
@ -36,21 +38,22 @@ pub struct Composer {
|
|||
pub role: Role,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Boxed, Clone, Debug)]
|
||||
#[boxed_type(name = "MusicusEnsemble")]
|
||||
pub struct Ensemble {
|
||||
pub ensemble_id: String,
|
||||
pub name: TranslatedString,
|
||||
pub persons: Vec<(Person, Instrument)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Boxed, Clone, Debug)]
|
||||
#[boxed_type(name = "MusicusRecording")]
|
||||
pub struct Recording {
|
||||
pub recording_id: String,
|
||||
pub work: Work,
|
||||
pub year: Option<i32>,
|
||||
pub persons: Vec<Performer>,
|
||||
pub ensembles: Vec<Ensemble>,
|
||||
pub tracks: Vec<Track>,
|
||||
pub ensembles: Vec<EnsemblePerformer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -60,6 +63,12 @@ pub struct Performer {
|
|||
pub instrument: Option<Instrument>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnsemblePerformer {
|
||||
pub ensemble: Ensemble,
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Track {
|
||||
pub track_id: String,
|
||||
|
|
@ -229,11 +238,7 @@ impl Display for Ensemble {
|
|||
}
|
||||
|
||||
impl Recording {
|
||||
pub fn from_table(
|
||||
data: tables::Recording,
|
||||
library_path: &str,
|
||||
connection: &mut SqliteConnection,
|
||||
) -> Result<Self> {
|
||||
pub fn from_table(data: tables::Recording, connection: &mut SqliteConnection) -> Result<Self> {
|
||||
let work = Work::from_table(
|
||||
works::table
|
||||
.filter(works::work_id.eq(&data.work_id))
|
||||
|
|
@ -249,24 +254,15 @@ impl Recording {
|
|||
.map(|r| Performer::from_table(r, connection))
|
||||
.collect::<Result<Vec<Performer>>>()?;
|
||||
|
||||
let ensembles: Vec<Ensemble> = ensembles::table
|
||||
let ensembles = ensembles::table
|
||||
.inner_join(recording_ensembles::table)
|
||||
.order(recording_ensembles::sequence_number)
|
||||
.filter(recording_ensembles::recording_id.eq(&data.recording_id))
|
||||
.select(tables::Ensemble::as_select())
|
||||
.load::<tables::Ensemble>(connection)?
|
||||
.select(tables::RecordingEnsemble::as_select())
|
||||
.load::<tables::RecordingEnsemble>(connection)?
|
||||
.into_iter()
|
||||
.map(|e| Ensemble::from_table(e, connection))
|
||||
.collect::<Result<Vec<Ensemble>>>()?;
|
||||
|
||||
let tracks: Vec<Track> = tracks::table
|
||||
.order(tracks::recording_index)
|
||||
.filter(tracks::recording_id.eq(&data.recording_id))
|
||||
.select(tables::Track::as_select())
|
||||
.load::<tables::Track>(connection)?
|
||||
.into_iter()
|
||||
.map(|t| Track::from_table(t, library_path, connection))
|
||||
.collect::<Result<Vec<Track>>>()?;
|
||||
.map(|e| EnsemblePerformer::from_table(e, connection))
|
||||
.collect::<Result<Vec<EnsemblePerformer>>>()?;
|
||||
|
||||
Ok(Self {
|
||||
recording_id: data.recording_id,
|
||||
|
|
@ -274,7 +270,6 @@ impl Recording {
|
|||
year: data.year,
|
||||
persons,
|
||||
ensembles,
|
||||
tracks,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +284,7 @@ impl Recording {
|
|||
&mut self
|
||||
.ensembles
|
||||
.iter()
|
||||
.map(|e| e.name.get().to_string())
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
|
||||
|
|
@ -344,12 +339,33 @@ impl Display for Performer {
|
|||
}
|
||||
}
|
||||
|
||||
impl Track {
|
||||
impl EnsemblePerformer {
|
||||
pub fn from_table(
|
||||
data: tables::Track,
|
||||
library_path: &str,
|
||||
data: tables::RecordingEnsemble,
|
||||
connection: &mut SqliteConnection,
|
||||
) -> Result<Self> {
|
||||
let ensemble_data = ensembles::table
|
||||
.filter(ensembles::ensemble_id.eq(&data.ensemble_id))
|
||||
.first::<tables::Ensemble>(connection)?;
|
||||
|
||||
let ensemble = Ensemble::from_table(ensemble_data, connection)?;
|
||||
|
||||
let role: Role = roles::table
|
||||
.filter(roles::role_id.eq(&data.role_id))
|
||||
.first(connection)?;
|
||||
|
||||
Ok(Self { ensemble, role })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EnsemblePerformer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.ensemble.name.get().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Track {
|
||||
pub fn from_table(data: tables::Track, connection: &mut SqliteConnection) -> Result<Self> {
|
||||
let works: Vec<Work> = works::table
|
||||
.inner_join(track_works::table)
|
||||
.order(track_works::sequence_number)
|
||||
|
|
@ -362,11 +378,7 @@ impl Track {
|
|||
|
||||
Ok(Self {
|
||||
track_id: data.track_id,
|
||||
path: Path::new(library_path)
|
||||
.join(&data.path)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
path: data.path,
|
||||
works,
|
||||
})
|
||||
}
|
||||
|
|
@ -377,4 +389,4 @@ impl PartialEq for Album {
|
|||
fn eq(&self, other: &Self) -> bool {
|
||||
self.album_id == other.album_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub struct Role {
|
|||
}
|
||||
|
||||
#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)]
|
||||
#[boxed_type(name = "MusicusInstrument")]
|
||||
#[boxed_type(name = "MusicusInstrument", nullable)]
|
||||
#[diesel(check_for_backend(Sqlite))]
|
||||
pub struct Instrument {
|
||||
pub instrument_id: String,
|
||||
|
|
@ -183,4 +183,4 @@ pub struct AlbumMedium {
|
|||
pub album_id: String,
|
||||
pub medium_id: String,
|
||||
pub sequence_number: i32,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
112
src/editor/ensemble_editor.rs
Normal file
112
src/editor/ensemble_editor.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use std::cell::OnceCell;
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::glib::{self, subclass::Signal};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{
|
||||
db::models::Ensemble, editor::translation_editor::MusicusTranslationEditor,
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||
#[template(file = "data/ui/ensemble_editor.blp")]
|
||||
pub struct MusicusEnsembleEditor {
|
||||
pub navigation: OnceCell<adw::NavigationView>,
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
pub ensemble_id: OnceCell<String>,
|
||||
|
||||
#[template_child]
|
||||
pub name_editor: TemplateChild<MusicusTranslationEditor>,
|
||||
#[template_child]
|
||||
pub save_button: TemplateChild<gtk::Button>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusEnsembleEditor {
|
||||
const NAME: &'static str = "MusicusEnsembleEditor";
|
||||
type Type = super::MusicusEnsembleEditor;
|
||||
type ParentType = adw::NavigationPage;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
MusicusTranslationEditor::static_type();
|
||||
klass.bind_template();
|
||||
klass.bind_template_instance_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for MusicusEnsembleEditor {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![Signal::builder("created")
|
||||
.param_types([Ensemble::static_type()])
|
||||
.build()]
|
||||
});
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusEnsembleEditor {}
|
||||
impl NavigationPageImpl for MusicusEnsembleEditor {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusEnsembleEditor(ObjectSubclass<imp::MusicusEnsembleEditor>)
|
||||
@extends gtk::Widget, adw::NavigationPage;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusEnsembleEditor {
|
||||
pub fn new(
|
||||
navigation: &adw::NavigationView,
|
||||
library: &MusicusLibrary,
|
||||
ensemble: Option<&Ensemble>,
|
||||
) -> Self {
|
||||
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(ensemble) = ensemble {
|
||||
obj.imp().save_button.set_label(&gettext("Save changes"));
|
||||
obj.imp().ensemble_id.set(ensemble.ensemble_id.clone()).unwrap();
|
||||
obj.imp().name_editor.set_translation(&ensemble.name);
|
||||
}
|
||||
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn connect_created<F: Fn(&Self, Ensemble) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("created", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
let ensemble = values[1].get::<Ensemble>().unwrap();
|
||||
f(&obj, ensemble);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn save(&self, _: >k::Button) {
|
||||
let library = self.imp().library.get().unwrap();
|
||||
let name = self.imp().name_editor.translation();
|
||||
|
||||
if let Some(ensemble_id) = self.imp().ensemble_id.get() {
|
||||
library.update_ensemble(ensemble_id, name).unwrap();
|
||||
} else {
|
||||
let ensemble = library.create_ensemble(name).unwrap();
|
||||
self.emit_by_name::<()>("created", &[&ensemble]);
|
||||
}
|
||||
|
||||
self.imp().navigation.get().unwrap().pop();
|
||||
}
|
||||
}
|
||||
204
src/editor/ensemble_selector_popover.rs
Normal file
204
src/editor/ensemble_selector_popover.rs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
use crate::{db::models::Ensemble, library::MusicusLibrary};
|
||||
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
glib::{self, subclass::Signal, Properties},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
use super::activatable_row::MusicusActivatableRow;
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
|
||||
#[properties(wrapper_type = super::MusicusEnsembleSelectorPopover)]
|
||||
#[template(file = "data/ui/ensemble_selector_popover.blp")]
|
||||
pub struct MusicusEnsembleSelectorPopover {
|
||||
#[property(get, construct_only)]
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
|
||||
pub ensembles: RefCell<Vec<Ensemble>>,
|
||||
|
||||
#[template_child]
|
||||
pub search_entry: TemplateChild<gtk::SearchEntry>,
|
||||
#[template_child]
|
||||
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
|
||||
#[template_child]
|
||||
pub list_box: TemplateChild<gtk::ListBox>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusEnsembleSelectorPopover {
|
||||
const NAME: &'static str = "MusicusEnsembleSelectorPopover";
|
||||
type Type = super::MusicusEnsembleSelectorPopover;
|
||||
type ParentType = gtk::Popover;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_instance_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for MusicusEnsembleSelectorPopover {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.obj()
|
||||
.connect_visible_notify(|obj: &super::MusicusEnsembleSelectorPopover| {
|
||||
if obj.is_visible() {
|
||||
obj.imp().search_entry.set_text("");
|
||||
obj.imp().search_entry.grab_focus();
|
||||
obj.imp().scrolled_window.vadjustment().set_value(0.0);
|
||||
}
|
||||
});
|
||||
|
||||
self.obj().search("");
|
||||
}
|
||||
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![
|
||||
Signal::builder("ensemble-selected")
|
||||
.param_types([Ensemble::static_type()])
|
||||
.build(),
|
||||
Signal::builder("create").build(),
|
||||
]
|
||||
});
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusEnsembleSelectorPopover {
|
||||
// TODO: Fix focus.
|
||||
fn focus(&self, direction_type: gtk::DirectionType) -> bool {
|
||||
if direction_type == gtk::DirectionType::Down {
|
||||
self.list_box.child_focus(direction_type)
|
||||
} else {
|
||||
self.parent_focus(direction_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PopoverImpl for MusicusEnsembleSelectorPopover {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusEnsembleSelectorPopover(ObjectSubclass<imp::MusicusEnsembleSelectorPopover>)
|
||||
@extends gtk::Widget, gtk::Popover;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusEnsembleSelectorPopover {
|
||||
pub fn new(library: &MusicusLibrary) -> Self {
|
||||
glib::Object::builder().property("library", library).build()
|
||||
}
|
||||
|
||||
pub fn connect_ensemble_selected<F: Fn(&Self, Ensemble) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
self.connect_local("ensemble-selected", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
let ensemble = values[1].get::<Ensemble>().unwrap();
|
||||
f(&obj, ensemble);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
fn search_changed(&self, entry: >k::SearchEntry) {
|
||||
self.search(&entry.text());
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn activate(&self, _: >k::SearchEntry) {
|
||||
if let Some(ensemble) = self.imp().ensembles.borrow().first() {
|
||||
self.select(ensemble.clone());
|
||||
} else {
|
||||
self.create();
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn stop_search(&self, _: >k::SearchEntry) {
|
||||
self.popdown();
|
||||
}
|
||||
|
||||
fn search(&self, search: &str) {
|
||||
let imp = self.imp();
|
||||
|
||||
let ensembles = imp
|
||||
.library
|
||||
.get()
|
||||
.unwrap()
|
||||
.search_ensembles(search)
|
||||
.unwrap();
|
||||
|
||||
imp.list_box.remove_all();
|
||||
|
||||
for ensemble in &ensembles {
|
||||
let row = MusicusActivatableRow::new(
|
||||
>k::Label::builder()
|
||||
.label(ensemble.to_string())
|
||||
.halign(gtk::Align::Start)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let ensemble = ensemble.clone();
|
||||
let obj = self.clone();
|
||||
row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.select(ensemble.clone());
|
||||
});
|
||||
|
||||
imp.list_box.append(&row);
|
||||
}
|
||||
|
||||
let create_box = gtk::Box::builder().spacing(12).build();
|
||||
create_box.append(>k::Image::builder().icon_name("list-add-symbolic").build());
|
||||
create_box.append(
|
||||
>k::Label::builder()
|
||||
.label(gettext("Create new ensemble"))
|
||||
.halign(gtk::Align::Start)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let create_row = MusicusActivatableRow::new(&create_box);
|
||||
let obj = self.clone();
|
||||
create_row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.create();
|
||||
});
|
||||
|
||||
imp.list_box.append(&create_row);
|
||||
|
||||
imp.ensembles.replace(ensembles);
|
||||
}
|
||||
|
||||
fn select(&self, ensemble: Ensemble) {
|
||||
self.emit_by_name::<()>("ensemble-selected", &[&ensemble]);
|
||||
self.popdown();
|
||||
}
|
||||
|
||||
fn create(&self) {
|
||||
self.emit_by_name::<()>("create", &[]);
|
||||
self.popdown();
|
||||
}
|
||||
}
|
||||
112
src/editor/instrument_editor.rs
Normal file
112
src/editor/instrument_editor.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use std::cell::OnceCell;
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::glib::{self, subclass::Signal};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{
|
||||
db::models::Instrument, editor::translation_editor::MusicusTranslationEditor,
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||
#[template(file = "data/ui/instrument_editor.blp")]
|
||||
pub struct MusicusInstrumentEditor {
|
||||
pub navigation: OnceCell<adw::NavigationView>,
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
pub instrument_id: OnceCell<String>,
|
||||
|
||||
#[template_child]
|
||||
pub name_editor: TemplateChild<MusicusTranslationEditor>,
|
||||
#[template_child]
|
||||
pub save_button: TemplateChild<gtk::Button>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusInstrumentEditor {
|
||||
const NAME: &'static str = "MusicusInstrumentEditor";
|
||||
type Type = super::MusicusInstrumentEditor;
|
||||
type ParentType = adw::NavigationPage;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
MusicusTranslationEditor::static_type();
|
||||
klass.bind_template();
|
||||
klass.bind_template_instance_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for MusicusInstrumentEditor {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![Signal::builder("created")
|
||||
.param_types([Instrument::static_type()])
|
||||
.build()]
|
||||
});
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusInstrumentEditor {}
|
||||
impl NavigationPageImpl for MusicusInstrumentEditor {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusInstrumentEditor(ObjectSubclass<imp::MusicusInstrumentEditor>)
|
||||
@extends gtk::Widget, adw::NavigationPage;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusInstrumentEditor {
|
||||
pub fn new(
|
||||
navigation: &adw::NavigationView,
|
||||
library: &MusicusLibrary,
|
||||
instrument: Option<&Instrument>,
|
||||
) -> Self {
|
||||
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(instrument) = instrument {
|
||||
obj.imp().save_button.set_label(&gettext("Save changes"));
|
||||
obj.imp().instrument_id.set(instrument.instrument_id.clone()).unwrap();
|
||||
obj.imp().name_editor.set_translation(&instrument.name);
|
||||
}
|
||||
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn connect_created<F: Fn(&Self, Instrument) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("created", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
let instrument = values[1].get::<Instrument>().unwrap();
|
||||
f(&obj, instrument);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn save(&self, _: >k::Button) {
|
||||
let library = self.imp().library.get().unwrap();
|
||||
let name = self.imp().name_editor.translation();
|
||||
|
||||
if let Some(instrument_id) = self.imp().instrument_id.get() {
|
||||
library.update_instrument(instrument_id, name).unwrap();
|
||||
} else {
|
||||
let instrument = library.create_instrument(name).unwrap();
|
||||
self.emit_by_name::<()>("created", &[&instrument]);
|
||||
}
|
||||
|
||||
self.imp().navigation.get().unwrap().pop();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,18 @@
|
|||
pub mod activatable_row;
|
||||
pub mod ensemble_editor;
|
||||
pub mod ensemble_selector_popover;
|
||||
pub mod instrument_editor;
|
||||
pub mod instrument_selector_popover;
|
||||
pub mod performer_role_selector_popover;
|
||||
pub mod person_editor;
|
||||
pub mod person_selector_popover;
|
||||
pub mod recording_editor;
|
||||
pub mod recording_editor_ensemble_row;
|
||||
pub mod recording_editor_performer_row;
|
||||
pub mod role_editor;
|
||||
pub mod role_selector_popover;
|
||||
pub mod translation_editor;
|
||||
pub mod translation_entry;
|
||||
pub mod work_editor;
|
||||
pub mod work_editor_composer_row;
|
||||
pub mod work_editor_composer_row;
|
||||
pub mod work_selector_popover;
|
||||
|
|
|
|||
328
src/editor/performer_role_selector_popover.rs
Normal file
328
src/editor/performer_role_selector_popover.rs
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
use crate::{
|
||||
db::models::{Instrument, Role},
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
glib::{self, subclass::Signal, Properties},
|
||||
pango,
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
use super::activatable_row::MusicusActivatableRow;
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
|
||||
#[properties(wrapper_type = super::MusicusPerformerRoleSelectorPopover)]
|
||||
#[template(file = "data/ui/performer_role_selector_popover.blp")]
|
||||
pub struct MusicusPerformerRoleSelectorPopover {
|
||||
#[property(get, construct_only)]
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
|
||||
pub roles: RefCell<Vec<Role>>,
|
||||
pub instruments: RefCell<Vec<Instrument>>,
|
||||
|
||||
#[template_child]
|
||||
pub stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
pub role_view: TemplateChild<adw::ToolbarView>,
|
||||
#[template_child]
|
||||
pub role_search_entry: TemplateChild<gtk::SearchEntry>,
|
||||
#[template_child]
|
||||
pub role_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
|
||||
#[template_child]
|
||||
pub role_list: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub instrument_view: TemplateChild<adw::ToolbarView>,
|
||||
#[template_child]
|
||||
pub instrument_search_entry: TemplateChild<gtk::SearchEntry>,
|
||||
#[template_child]
|
||||
pub instrument_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
|
||||
#[template_child]
|
||||
pub instrument_list: TemplateChild<gtk::ListBox>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusPerformerRoleSelectorPopover {
|
||||
const NAME: &'static str = "MusicusPerformerRoleSelectorPopover";
|
||||
type Type = super::MusicusPerformerRoleSelectorPopover;
|
||||
type ParentType = gtk::Popover;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_instance_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for MusicusPerformerRoleSelectorPopover {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.obj().connect_visible_notify(
|
||||
|obj: &super::MusicusPerformerRoleSelectorPopover| {
|
||||
if obj.is_visible() {
|
||||
obj.imp().stack.set_visible_child(&*obj.imp().role_view);
|
||||
obj.imp().role_search_entry.set_text("");
|
||||
obj.imp().role_search_entry.grab_focus();
|
||||
obj.imp().role_scrolled_window.vadjustment().set_value(0.0);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.obj().search_roles("");
|
||||
}
|
||||
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![
|
||||
Signal::builder("selected")
|
||||
.param_types([Role::static_type(), Instrument::static_type()])
|
||||
.build(),
|
||||
Signal::builder("create-role").build(),
|
||||
Signal::builder("create-instrument").build(),
|
||||
]
|
||||
});
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusPerformerRoleSelectorPopover {
|
||||
// TODO: Fix focus.
|
||||
fn focus(&self, direction_type: gtk::DirectionType) -> bool {
|
||||
if direction_type == gtk::DirectionType::Down {
|
||||
if self.stack.visible_child() == Some(self.role_list.get().upcast()) {
|
||||
self.role_list.child_focus(direction_type)
|
||||
} else {
|
||||
self.instrument_list.child_focus(direction_type)
|
||||
}
|
||||
} else {
|
||||
self.parent_focus(direction_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PopoverImpl for MusicusPerformerRoleSelectorPopover {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusPerformerRoleSelectorPopover(ObjectSubclass<imp::MusicusPerformerRoleSelectorPopover>)
|
||||
@extends gtk::Widget, gtk::Popover;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusPerformerRoleSelectorPopover {
|
||||
pub fn new(library: &MusicusLibrary) -> Self {
|
||||
glib::Object::builder().property("library", library).build()
|
||||
}
|
||||
|
||||
pub fn connect_selected<F: Fn(&Self, Role, Option<Instrument>) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
self.connect_local("selected", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
let role = values[1].get::<Role>().unwrap();
|
||||
let instrument = values[2].get::<Option<Instrument>>().unwrap();
|
||||
f(&obj, role, instrument);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn connect_create_role<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("create-role", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
f(&obj);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn connect_create_instrument<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("create-instrument", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
f(&obj);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn role_search_changed(&self, entry: >k::SearchEntry) {
|
||||
self.search_roles(&entry.text());
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn role_activate(&self, _: >k::SearchEntry) {
|
||||
if let Some(role) = self.imp().roles.borrow().first() {
|
||||
self.select_role(role.to_owned());
|
||||
} else {
|
||||
self.create_role();
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn back_button_clicked(&self, _: >k::Button) {
|
||||
self.imp().stack.set_visible_child(&*self.imp().role_view);
|
||||
self.imp().role_search_entry.grab_focus();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn instrument_search_changed(&self, entry: >k::SearchEntry) {
|
||||
self.search_instruments(&entry.text());
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn instrument_activate(&self, _: >k::SearchEntry) {
|
||||
if let Some(instrument) = self.imp().instruments.borrow().first() {
|
||||
self.select_instrument(instrument.clone());
|
||||
} else {
|
||||
self.create_instrument();
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn stop_search(&self, _: >k::SearchEntry) {
|
||||
self.popdown();
|
||||
}
|
||||
|
||||
fn search_roles(&self, search: &str) {
|
||||
let imp = self.imp();
|
||||
|
||||
let roles = imp.library.get().unwrap().search_roles(search).unwrap();
|
||||
|
||||
imp.role_list.remove_all();
|
||||
|
||||
for role in &roles {
|
||||
let row = MusicusActivatableRow::new(
|
||||
>k::Label::builder()
|
||||
.label(role.to_string())
|
||||
.halign(gtk::Align::Start)
|
||||
.ellipsize(pango::EllipsizeMode::Middle)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let role = role.clone();
|
||||
let obj = self.clone();
|
||||
row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.select_role(role.clone());
|
||||
});
|
||||
|
||||
imp.role_list.append(&row);
|
||||
}
|
||||
|
||||
let create_box = gtk::Box::builder().spacing(12).build();
|
||||
create_box.append(>k::Image::builder().icon_name("list-add-symbolic").build());
|
||||
create_box.append(
|
||||
>k::Label::builder()
|
||||
.label(gettext("Create new role"))
|
||||
.halign(gtk::Align::Start)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let create_row = MusicusActivatableRow::new(&create_box);
|
||||
let obj = self.clone();
|
||||
create_row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.create_role();
|
||||
});
|
||||
|
||||
imp.role_list.append(&create_row);
|
||||
|
||||
imp.roles.replace(roles);
|
||||
}
|
||||
|
||||
fn search_instruments(&self, search: &str) {
|
||||
let imp = self.imp();
|
||||
|
||||
let instruments = imp
|
||||
.library
|
||||
.get()
|
||||
.unwrap()
|
||||
.search_instruments(search)
|
||||
.unwrap();
|
||||
|
||||
imp.instrument_list.remove_all();
|
||||
|
||||
for instrument in &instruments {
|
||||
let row = MusicusActivatableRow::new(
|
||||
>k::Label::builder()
|
||||
.label(instrument.name.get())
|
||||
.halign(gtk::Align::Start)
|
||||
.ellipsize(pango::EllipsizeMode::Middle)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let instrument = instrument.clone();
|
||||
let obj = self.clone();
|
||||
row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.select_instrument(instrument.clone());
|
||||
});
|
||||
|
||||
imp.instrument_list.append(&row);
|
||||
}
|
||||
|
||||
let create_box = gtk::Box::builder().spacing(12).build();
|
||||
create_box.append(>k::Image::builder().icon_name("list-add-symbolic").build());
|
||||
create_box.append(
|
||||
>k::Label::builder()
|
||||
.label(gettext("Create new instrument"))
|
||||
.halign(gtk::Align::Start)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let create_row = MusicusActivatableRow::new(&create_box);
|
||||
let obj = self.clone();
|
||||
create_row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.create_instrument();
|
||||
});
|
||||
|
||||
imp.instrument_list.append(&create_row);
|
||||
|
||||
imp.instruments.replace(instruments);
|
||||
}
|
||||
|
||||
fn select_role(&self, role: Role) {
|
||||
if role == self.library().performer_default_role().unwrap() {
|
||||
self.imp().instrument_search_entry.set_text("");
|
||||
self.imp().instrument_search_entry.grab_focus();
|
||||
self.imp()
|
||||
.instrument_scrolled_window
|
||||
.vadjustment()
|
||||
.set_value(0.0);
|
||||
self.imp()
|
||||
.stack
|
||||
.set_visible_child(&*self.imp().instrument_view);
|
||||
|
||||
self.search_instruments("");
|
||||
} else {
|
||||
self.emit_by_name::<()>("selected", &[&role, &None::<Instrument>]);
|
||||
self.popdown();
|
||||
}
|
||||
}
|
||||
|
||||
fn select_instrument(&self, instrument: Instrument) {
|
||||
let role = self.library().performer_default_role().unwrap();
|
||||
self.emit_by_name::<()>("selected", &[&role, &instrument]);
|
||||
self.popdown();
|
||||
}
|
||||
|
||||
fn create_role(&self) {
|
||||
self.emit_by_name::<()>("create-role", &[]);
|
||||
self.popdown();
|
||||
}
|
||||
|
||||
fn create_instrument(&self) {
|
||||
self.emit_by_name::<()>("create-instrument", &[]);
|
||||
self.popdown();
|
||||
}
|
||||
}
|
||||
317
src/editor/recording_editor.rs
Normal file
317
src/editor/recording_editor.rs
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::glib::{self, clone, subclass::Signal, Properties};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{
|
||||
db::models::{Ensemble, EnsemblePerformer, Performer, Person, Recording, Work},
|
||||
editor::{
|
||||
ensemble_editor::MusicusEnsembleEditor,
|
||||
ensemble_selector_popover::MusicusEnsembleSelectorPopover,
|
||||
person_editor::MusicusPersonEditor, person_selector_popover::MusicusPersonSelectorPopover,
|
||||
recording_editor_ensemble_row::MusicusRecordingEditorEnsembleRow,
|
||||
recording_editor_performer_row::MusicusRecordingEditorPerformerRow,
|
||||
work_selector_popover::MusicusWorkSelectorPopover,
|
||||
},
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use crate::editor::work_editor::MusicusWorkEditor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
|
||||
#[properties(wrapper_type = super::MusicusRecordingEditor)]
|
||||
#[template(file = "data/ui/recording_editor.blp")]
|
||||
pub struct MusicusRecordingEditor {
|
||||
#[property(get, construct_only)]
|
||||
pub navigation: OnceCell<adw::NavigationView>,
|
||||
|
||||
#[property(get, construct_only)]
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
|
||||
pub recording_id: OnceCell<String>,
|
||||
|
||||
pub work: RefCell<Option<Work>>,
|
||||
pub performer_rows: RefCell<Vec<MusicusRecordingEditorPerformerRow>>,
|
||||
pub ensemble_rows: RefCell<Vec<MusicusRecordingEditorEnsembleRow>>,
|
||||
|
||||
pub work_selector_popover: OnceCell<MusicusWorkSelectorPopover>,
|
||||
pub persons_popover: OnceCell<MusicusPersonSelectorPopover>,
|
||||
pub ensembles_popover: OnceCell<MusicusEnsembleSelectorPopover>,
|
||||
|
||||
#[template_child]
|
||||
pub work_row: TemplateChild<adw::ActionRow>,
|
||||
#[template_child]
|
||||
pub select_work_box: TemplateChild<gtk::Box>,
|
||||
#[template_child]
|
||||
pub year_row: TemplateChild<adw::SpinRow>,
|
||||
#[template_child]
|
||||
pub performer_list: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub select_person_box: TemplateChild<gtk::Box>,
|
||||
#[template_child]
|
||||
pub ensemble_list: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub select_ensemble_box: TemplateChild<gtk::Box>,
|
||||
#[template_child]
|
||||
pub save_button: TemplateChild<gtk::Button>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusRecordingEditor {
|
||||
const NAME: &'static str = "MusicusRecordingEditor";
|
||||
type Type = super::MusicusRecordingEditor;
|
||||
type ParentType = adw::NavigationPage;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_instance_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for MusicusRecordingEditor {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![Signal::builder("created")
|
||||
.param_types([Recording::static_type()])
|
||||
.build()]
|
||||
});
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let work_selector_popover =
|
||||
MusicusWorkSelectorPopover::new(self.library.get().unwrap());
|
||||
|
||||
let obj = self.obj().clone();
|
||||
work_selector_popover.connect_selected(move |_, work| {
|
||||
obj.set_work(work);
|
||||
});
|
||||
|
||||
let obj = self.obj().clone();
|
||||
work_selector_popover.connect_create(move |_| {
|
||||
let editor = MusicusWorkEditor::new(&obj.navigation(), &obj.library(), None);
|
||||
|
||||
editor.connect_created(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, work| {
|
||||
obj.set_work(work);
|
||||
}
|
||||
));
|
||||
|
||||
obj.navigation().push(&editor);
|
||||
});
|
||||
|
||||
self.select_work_box.append(&work_selector_popover);
|
||||
self.work_selector_popover
|
||||
.set(work_selector_popover)
|
||||
.unwrap();
|
||||
|
||||
let persons_popover = MusicusPersonSelectorPopover::new(self.library.get().unwrap());
|
||||
|
||||
let obj = self.obj().clone();
|
||||
persons_popover.connect_person_selected(move |_, person| {
|
||||
obj.add_performer(person);
|
||||
});
|
||||
|
||||
let obj = self.obj().clone();
|
||||
persons_popover.connect_create(move |_| {
|
||||
let editor = MusicusPersonEditor::new(&obj.navigation(), &obj.library(), None);
|
||||
|
||||
editor.connect_created(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, person| {
|
||||
obj.add_performer(person);
|
||||
}
|
||||
));
|
||||
|
||||
obj.navigation().push(&editor);
|
||||
});
|
||||
|
||||
self.select_person_box.append(&persons_popover);
|
||||
self.persons_popover.set(persons_popover).unwrap();
|
||||
|
||||
let ensembles_popover =
|
||||
MusicusEnsembleSelectorPopover::new(self.library.get().unwrap());
|
||||
|
||||
let obj = self.obj().clone();
|
||||
ensembles_popover.connect_ensemble_selected(move |_, ensemble| {
|
||||
obj.add_ensemble(ensemble);
|
||||
});
|
||||
|
||||
let obj = self.obj().clone();
|
||||
ensembles_popover.connect_create(move |_| {
|
||||
let editor = MusicusEnsembleEditor::new(&obj.navigation(), &obj.library(), None);
|
||||
|
||||
editor.connect_created(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, ensemble| {
|
||||
obj.add_ensemble(ensemble);
|
||||
}
|
||||
));
|
||||
|
||||
obj.navigation().push(&editor);
|
||||
});
|
||||
|
||||
self.select_ensemble_box.append(&ensembles_popover);
|
||||
self.ensembles_popover.set(ensembles_popover).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusRecordingEditor {}
|
||||
impl NavigationPageImpl for MusicusRecordingEditor {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusRecordingEditor(ObjectSubclass<imp::MusicusRecordingEditor>)
|
||||
@extends gtk::Widget, adw::NavigationPage;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusRecordingEditor {
|
||||
pub fn new(
|
||||
navigation: &adw::NavigationView,
|
||||
library: &MusicusLibrary,
|
||||
recording: Option<&Recording>,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder()
|
||||
.property("navigation", navigation)
|
||||
.property("library", library)
|
||||
.build();
|
||||
|
||||
if let Some(recording) = recording {
|
||||
obj.imp().save_button.set_label(&gettext("Save changes"));
|
||||
obj.imp()
|
||||
.recording_id
|
||||
.set(recording.recording_id.clone())
|
||||
.unwrap();
|
||||
// TODO: Initialize data.
|
||||
}
|
||||
|
||||
obj
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn select_work(&self, _: &adw::ActionRow) {
|
||||
self.imp().work_selector_popover.get().unwrap().popup();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn select_person(&self, _: &adw::ActionRow) {
|
||||
self.imp().persons_popover.get().unwrap().popup();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn select_ensemble(&self, _: &adw::ActionRow) {
|
||||
self.imp().ensembles_popover.get().unwrap().popup();
|
||||
}
|
||||
|
||||
fn set_work(&self, work: Work) {
|
||||
self.imp().work_row.set_title(&work.name.get());
|
||||
self.imp().work_row.set_subtitle(&work.composers_string());
|
||||
self.imp().work.replace(Some(work));
|
||||
}
|
||||
|
||||
fn add_performer(&self, person: Person) {
|
||||
let role = self.library().performer_default_role().unwrap();
|
||||
let performer = Performer {
|
||||
person,
|
||||
role,
|
||||
instrument: None,
|
||||
};
|
||||
|
||||
let row =
|
||||
MusicusRecordingEditorPerformerRow::new(&self.navigation(), &self.library(), performer);
|
||||
|
||||
row.connect_remove(clone!(
|
||||
#[weak(rename_to = this)]
|
||||
self,
|
||||
move |row| {
|
||||
this.imp().performer_list.remove(row);
|
||||
this.imp().performer_rows.borrow_mut().retain(|c| c != row);
|
||||
}
|
||||
));
|
||||
|
||||
self.imp()
|
||||
.performer_list
|
||||
.insert(&row, self.imp().performer_rows.borrow().len() as i32);
|
||||
|
||||
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 };
|
||||
|
||||
let row =
|
||||
MusicusRecordingEditorEnsembleRow::new(&self.navigation(), &self.library(), performer);
|
||||
|
||||
row.connect_remove(clone!(
|
||||
#[weak(rename_to = this)]
|
||||
self,
|
||||
move |row| {
|
||||
this.imp().ensemble_list.remove(row);
|
||||
this.imp().ensemble_rows.borrow_mut().retain(|c| c != row);
|
||||
}
|
||||
));
|
||||
|
||||
self.imp()
|
||||
.ensemble_list
|
||||
.insert(&row, self.imp().ensemble_rows.borrow().len() as i32);
|
||||
|
||||
self.imp().ensemble_rows.borrow_mut().push(row);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn save(&self, _: >k::Button) {
|
||||
let library = self.imp().library.get().unwrap();
|
||||
|
||||
// TODO: No work selected?
|
||||
let work = self.imp().work.borrow().as_ref().unwrap().clone();
|
||||
let year = self.imp().year_row.value() as i32;
|
||||
|
||||
let performers = self
|
||||
.imp()
|
||||
.performer_rows
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|p| p.performer())
|
||||
.collect::<Vec<Performer>>();
|
||||
|
||||
let ensembles = self
|
||||
.imp()
|
||||
.ensemble_rows
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|e| e.ensemble())
|
||||
.collect::<Vec<EnsemblePerformer>>();
|
||||
|
||||
if let Some(recording_id) = self.imp().recording_id.get() {
|
||||
library
|
||||
.update_recording(recording_id, work, Some(year), performers, ensembles)
|
||||
.unwrap();
|
||||
} else {
|
||||
let recording = library
|
||||
.create_recording(work, Some(year), performers, ensembles)
|
||||
.unwrap();
|
||||
self.emit_by_name::<()>("created", &[&recording]);
|
||||
}
|
||||
|
||||
self.imp().navigation.get().unwrap().pop();
|
||||
}
|
||||
}
|
||||
149
src/editor/recording_editor_ensemble_row.rs
Normal file
149
src/editor/recording_editor_ensemble_row.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::glib::{self, clone, subclass::Signal, Properties};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{
|
||||
db::models::EnsemblePerformer,
|
||||
editor::{role_editor::MusicusRoleEditor, role_selector_popover::MusicusRoleSelectorPopover},
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
|
||||
#[properties(wrapper_type = super::MusicusRecordingEditorEnsembleRow)]
|
||||
#[template(file = "data/ui/recording_editor_ensemble_row.blp")]
|
||||
pub struct MusicusRecordingEditorEnsembleRow {
|
||||
#[property(get, construct_only)]
|
||||
pub navigation: OnceCell<adw::NavigationView>,
|
||||
|
||||
#[property(get, construct_only)]
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
|
||||
pub ensemble: RefCell<Option<EnsemblePerformer>>,
|
||||
pub role_popover: OnceCell<MusicusRoleSelectorPopover>,
|
||||
|
||||
#[template_child]
|
||||
pub role_label: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
pub role_box: TemplateChild<gtk::Box>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusRecordingEditorEnsembleRow {
|
||||
const NAME: &'static str = "MusicusRecordingEditorEnsembleRow";
|
||||
type Type = super::MusicusRecordingEditorEnsembleRow;
|
||||
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<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for MusicusRecordingEditorEnsembleRow {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> =
|
||||
Lazy::new(|| vec![Signal::builder("remove").build()]);
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let role_popover = MusicusRoleSelectorPopover::new(self.library.get().unwrap());
|
||||
|
||||
let obj = self.obj().to_owned();
|
||||
role_popover.connect_role_selected(move |_, role| {
|
||||
if let Some(ensemble) = &mut *obj.imp().ensemble.borrow_mut() {
|
||||
obj.imp().role_label.set_label(&role.to_string());
|
||||
ensemble.role = role;
|
||||
}
|
||||
});
|
||||
|
||||
let obj = self.obj().to_owned();
|
||||
role_popover.connect_create(move |_| {
|
||||
let editor = MusicusRoleEditor::new(&obj.navigation(), &obj.library(), None);
|
||||
|
||||
editor.connect_created(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, role| {
|
||||
if let Some(ensemble) = &mut *obj.imp().ensemble.borrow_mut() {
|
||||
obj.imp().role_label.set_label(&role.to_string());
|
||||
ensemble.role = role;
|
||||
};
|
||||
}
|
||||
));
|
||||
|
||||
obj.navigation().push(&editor);
|
||||
});
|
||||
|
||||
self.role_box.append(&role_popover);
|
||||
self.role_popover.set(role_popover).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusRecordingEditorEnsembleRow {}
|
||||
impl ListBoxRowImpl for MusicusRecordingEditorEnsembleRow {}
|
||||
impl PreferencesRowImpl for MusicusRecordingEditorEnsembleRow {}
|
||||
impl ActionRowImpl for MusicusRecordingEditorEnsembleRow {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusRecordingEditorEnsembleRow(ObjectSubclass<imp::MusicusRecordingEditorEnsembleRow>)
|
||||
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusRecordingEditorEnsembleRow {
|
||||
pub fn new(
|
||||
navigation: &adw::NavigationView,
|
||||
library: &MusicusLibrary,
|
||||
ensemble: EnsemblePerformer,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder()
|
||||
.property("navigation", navigation)
|
||||
.property("library", library)
|
||||
.build();
|
||||
obj.set_ensemble(ensemble);
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("remove", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
f(&obj);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ensemble(&self) -> EnsemblePerformer {
|
||||
self.imp().ensemble.borrow().to_owned().unwrap()
|
||||
}
|
||||
|
||||
fn set_ensemble(&self, ensemble: EnsemblePerformer) {
|
||||
self.set_title(&ensemble.ensemble.to_string());
|
||||
self.imp().role_label.set_label(&ensemble.role.to_string());
|
||||
self.imp().ensemble.replace(Some(ensemble));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn open_role_popover(&self, _: >k::Button) {
|
||||
self.imp().role_popover.get().unwrap().popup();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn remove(&self, _: >k::Button) {
|
||||
self.emit_by_name::<()>("remove", &[]);
|
||||
}
|
||||
}
|
||||
188
src/editor/recording_editor_performer_row.rs
Normal file
188
src/editor/recording_editor_performer_row.rs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::glib::{self, clone, subclass::Signal, Properties};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{
|
||||
db::models::Performer,
|
||||
editor::{
|
||||
performer_role_selector_popover::MusicusPerformerRoleSelectorPopover,
|
||||
role_editor::MusicusRoleEditor,
|
||||
},
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use crate::editor::instrument_editor::MusicusInstrumentEditor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
|
||||
#[properties(wrapper_type = super::MusicusRecordingEditorPerformerRow)]
|
||||
#[template(file = "data/ui/recording_editor_performer_row.blp")]
|
||||
pub struct MusicusRecordingEditorPerformerRow {
|
||||
#[property(get, construct_only)]
|
||||
pub navigation: OnceCell<adw::NavigationView>,
|
||||
|
||||
#[property(get, construct_only)]
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
|
||||
pub performer: RefCell<Option<Performer>>,
|
||||
pub role_popover: OnceCell<MusicusPerformerRoleSelectorPopover>,
|
||||
|
||||
#[template_child]
|
||||
pub role_label: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
pub role_box: TemplateChild<gtk::Box>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusRecordingEditorPerformerRow {
|
||||
const NAME: &'static str = "MusicusRecordingEditorPerformerRow";
|
||||
type Type = super::MusicusRecordingEditorPerformerRow;
|
||||
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<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for MusicusRecordingEditorPerformerRow {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> =
|
||||
Lazy::new(|| vec![Signal::builder("remove").build()]);
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let role_popover =
|
||||
MusicusPerformerRoleSelectorPopover::new(self.library.get().unwrap());
|
||||
|
||||
let obj = self.obj().to_owned();
|
||||
role_popover.connect_selected(move |_, role, instrument| {
|
||||
if let Some(performer) = &mut *obj.imp().performer.borrow_mut() {
|
||||
let label = match &instrument {
|
||||
Some(instrument) => instrument.to_string(),
|
||||
None => role.to_string(),
|
||||
};
|
||||
|
||||
obj.imp().role_label.set_label(&label);
|
||||
|
||||
performer.role = role;
|
||||
performer.instrument = instrument;
|
||||
}
|
||||
});
|
||||
|
||||
let obj = self.obj().to_owned();
|
||||
role_popover.connect_create_role(move |_| {
|
||||
let editor = MusicusRoleEditor::new(&obj.navigation(), &obj.library(), None);
|
||||
|
||||
editor.connect_created(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, role| {
|
||||
if let Some(performer) = &mut *obj.imp().performer.borrow_mut() {
|
||||
obj.imp().role_label.set_label(&role.to_string());
|
||||
performer.role = role;
|
||||
performer.instrument = None;
|
||||
};
|
||||
}
|
||||
));
|
||||
|
||||
obj.navigation().push(&editor);
|
||||
});
|
||||
|
||||
let obj = self.obj().to_owned();
|
||||
role_popover.connect_create_instrument(move |_| {
|
||||
let editor = MusicusInstrumentEditor::new(&obj.navigation(), &obj.library(), None);
|
||||
|
||||
editor.connect_created(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, instrument| {
|
||||
if let Some(performer) = &mut *obj.imp().performer.borrow_mut() {
|
||||
obj.imp().role_label.set_label(&instrument.to_string());
|
||||
performer.role = obj.library().performer_default_role().unwrap();
|
||||
performer.instrument = Some(instrument);
|
||||
};
|
||||
}
|
||||
));
|
||||
|
||||
obj.navigation().push(&editor);
|
||||
});
|
||||
|
||||
self.role_box.append(&role_popover);
|
||||
self.role_popover.set(role_popover).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusRecordingEditorPerformerRow {}
|
||||
impl ListBoxRowImpl for MusicusRecordingEditorPerformerRow {}
|
||||
impl PreferencesRowImpl for MusicusRecordingEditorPerformerRow {}
|
||||
impl ActionRowImpl for MusicusRecordingEditorPerformerRow {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusRecordingEditorPerformerRow(ObjectSubclass<imp::MusicusRecordingEditorPerformerRow>)
|
||||
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusRecordingEditorPerformerRow {
|
||||
pub fn new(
|
||||
navigation: &adw::NavigationView,
|
||||
library: &MusicusLibrary,
|
||||
performer: Performer,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder()
|
||||
.property("navigation", navigation)
|
||||
.property("library", library)
|
||||
.build();
|
||||
obj.set_performer(performer);
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("remove", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
f(&obj);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn performer(&self) -> Performer {
|
||||
self.imp().performer.borrow().to_owned().unwrap()
|
||||
}
|
||||
|
||||
fn set_performer(&self, performer: Performer) {
|
||||
self.set_title(&performer.person.to_string());
|
||||
|
||||
let label = match &performer.instrument {
|
||||
Some(instrument) => instrument.to_string(),
|
||||
None => performer.role.to_string(),
|
||||
};
|
||||
|
||||
self.imp().role_label.set_label(&label.to_string());
|
||||
self.imp().performer.replace(Some(performer));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn open_role_popover(&self, _: >k::Button) {
|
||||
self.imp().role_popover.get().unwrap().popup();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn remove(&self, _: >k::Button) {
|
||||
self.emit_by_name::<()>("remove", &[]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,20 @@
|
|||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::glib::{
|
||||
clone, Properties,
|
||||
{self, subclass::Signal},
|
||||
};
|
||||
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,
|
||||
|
|
@ -12,12 +23,6 @@ use crate::{
|
|||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::glib::{self, clone, Properties};
|
||||
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
|
|
@ -31,10 +36,13 @@ mod imp {
|
|||
#[property(get, construct_only)]
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
|
||||
pub work_id: OnceCell<String>,
|
||||
|
||||
// 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<Vec<MusicusWorkEditorComposerRow>>,
|
||||
// TODO: These need to be PartRows!
|
||||
pub parts: RefCell<Vec<WorkPart>>,
|
||||
pub instruments: RefCell<Vec<Instrument>>,
|
||||
|
||||
|
|
@ -53,6 +61,8 @@ mod imp {
|
|||
pub instrument_list: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub select_instrument_box: TemplateChild<gtk::Box>,
|
||||
#[template_child]
|
||||
pub save_button: TemplateChild<gtk::Button>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
|
@ -74,6 +84,16 @@ mod imp {
|
|||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for MusicusWorkEditor {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![Signal::builder("created")
|
||||
.param_types([Work::static_type()])
|
||||
.build()]
|
||||
});
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
|
|
@ -106,43 +126,24 @@ mod imp {
|
|||
MusicusInstrumentSelectorPopover::new(self.library.get().unwrap());
|
||||
|
||||
let obj = self.obj().clone();
|
||||
instruments_popover.connect_instrument_selected(
|
||||
move |_: &MusicusInstrumentSelectorPopover, instrument: Instrument| {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(instrument.to_string())
|
||||
.build();
|
||||
instruments_popover.connect_instrument_selected(move |_, instrument| {
|
||||
obj.add_instrument_row(instrument);
|
||||
});
|
||||
|
||||
let remove_button = gtk::Button::builder()
|
||||
.icon_name("user-trash-symbolic")
|
||||
.valign(gtk::Align::Center)
|
||||
.css_classes(["flat"])
|
||||
.build();
|
||||
let obj = self.obj().clone();
|
||||
instruments_popover.connect_create(move |_| {
|
||||
let editor = MusicusInstrumentEditor::new(&obj.navigation(), &obj.library(), None);
|
||||
|
||||
remove_button.connect_clicked(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
#[weak]
|
||||
row,
|
||||
#[strong]
|
||||
instrument,
|
||||
move |_| {
|
||||
obj.imp().instrument_list.remove(&row);
|
||||
obj.imp()
|
||||
.instruments
|
||||
.borrow_mut()
|
||||
.retain(|i| *i != instrument);
|
||||
}
|
||||
));
|
||||
editor.connect_created(clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, instrument| {
|
||||
obj.add_instrument_row(instrument);
|
||||
}
|
||||
));
|
||||
|
||||
row.add_suffix(&remove_button);
|
||||
|
||||
obj.imp()
|
||||
.instrument_list
|
||||
.insert(&row, obj.imp().instruments.borrow().len() as i32);
|
||||
|
||||
obj.imp().instruments.borrow_mut().push(instrument);
|
||||
},
|
||||
);
|
||||
obj.navigation().push(&editor);
|
||||
});
|
||||
|
||||
self.select_instrument_box.append(&instruments_popover);
|
||||
self.instruments_popover.set(instruments_popover).unwrap();
|
||||
|
|
@ -170,13 +171,24 @@ impl MusicusWorkEditor {
|
|||
.property("library", library)
|
||||
.build();
|
||||
|
||||
if let Some(_work) = work {
|
||||
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
|
||||
}
|
||||
|
||||
pub fn connect_created<F: Fn(&Self, Work) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("created", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
let work = values[1].get::<Work>().unwrap();
|
||||
f(&obj, work);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_person(&self, _: &adw::ActionRow) {
|
||||
self.imp().persons_popover.get().unwrap().popup();
|
||||
|
|
@ -246,4 +258,69 @@ impl MusicusWorkEditor {
|
|||
|
||||
self.imp().composer_rows.borrow_mut().push(row);
|
||||
}
|
||||
|
||||
fn add_instrument_row(&self, instrument: Instrument) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(instrument.to_string())
|
||||
.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!(
|
||||
#[weak(rename_to = this)]
|
||||
self,
|
||||
#[weak]
|
||||
row,
|
||||
#[strong]
|
||||
instrument,
|
||||
move |_| {
|
||||
this.imp().instrument_list.remove(&row);
|
||||
this.imp()
|
||||
.instruments
|
||||
.borrow_mut()
|
||||
.retain(|i| *i != instrument);
|
||||
}
|
||||
));
|
||||
|
||||
row.add_suffix(&remove_button);
|
||||
|
||||
self.imp()
|
||||
.instrument_list
|
||||
.insert(&row, self.imp().instruments.borrow().len() as i32);
|
||||
|
||||
self.imp().instruments.borrow_mut().push(instrument);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn save(&self, _: >k::Button) {
|
||||
let library = self.imp().library.get().unwrap();
|
||||
|
||||
let name = self.imp().name_editor.translation();
|
||||
let parts = self.imp().parts.borrow().clone();
|
||||
let composers = self
|
||||
.imp()
|
||||
.composer_rows
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|c| c.composer())
|
||||
.collect::<Vec<Composer>>();
|
||||
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();
|
||||
} else {
|
||||
let work = library
|
||||
.create_work(name, parts, composers, instruments)
|
||||
.unwrap();
|
||||
self.emit_by_name::<()>("created", &[&work]);
|
||||
}
|
||||
|
||||
self.imp().navigation.get().unwrap().pop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use gtk::glib::{self, clone, subclass::Signal, Properties};
|
|||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{
|
||||
db::models::{Composer, Role},
|
||||
db::models::Composer,
|
||||
editor::{role_editor::MusicusRoleEditor, role_selector_popover::MusicusRoleSelectorPopover},
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
|
@ -142,14 +142,6 @@ impl MusicusWorkEditorComposerRow {
|
|||
self.imp().role_popover.get().unwrap().popup();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn role_selected(&self, role: Role) {
|
||||
if let Some(composer) = &mut *self.imp().composer.borrow_mut() {
|
||||
self.imp().role_label.set_label(&role.to_string());
|
||||
composer.role = role;
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn remove(&self, _: >k::Button) {
|
||||
self.emit_by_name::<()>("remove", &[]);
|
||||
|
|
|
|||
308
src/editor/work_selector_popover.rs
Normal file
308
src/editor/work_selector_popover.rs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
use crate::{
|
||||
db::models::{Person, Work},
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
glib::{self, subclass::Signal, Properties},
|
||||
pango,
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
|
||||
use super::activatable_row::MusicusActivatableRow;
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
|
||||
#[properties(wrapper_type = super::MusicusWorkSelectorPopover)]
|
||||
#[template(file = "data/ui/work_selector_popover.blp")]
|
||||
pub struct MusicusWorkSelectorPopover {
|
||||
#[property(get, construct_only)]
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
|
||||
pub composers: RefCell<Vec<Person>>,
|
||||
pub composer: RefCell<Option<Person>>,
|
||||
pub works: RefCell<Vec<Work>>,
|
||||
|
||||
#[template_child]
|
||||
pub stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
pub composer_view: TemplateChild<adw::ToolbarView>,
|
||||
#[template_child]
|
||||
pub composer_search_entry: TemplateChild<gtk::SearchEntry>,
|
||||
#[template_child]
|
||||
pub composer_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
|
||||
#[template_child]
|
||||
pub composer_list: TemplateChild<gtk::ListBox>,
|
||||
#[template_child]
|
||||
pub work_view: TemplateChild<adw::ToolbarView>,
|
||||
#[template_child]
|
||||
pub composer_label: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
pub work_search_entry: TemplateChild<gtk::SearchEntry>,
|
||||
#[template_child]
|
||||
pub work_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
|
||||
#[template_child]
|
||||
pub work_list: TemplateChild<gtk::ListBox>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MusicusWorkSelectorPopover {
|
||||
const NAME: &'static str = "MusicusWorkSelectorPopover";
|
||||
type Type = super::MusicusWorkSelectorPopover;
|
||||
type ParentType = gtk::Popover;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_instance_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for MusicusWorkSelectorPopover {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.obj()
|
||||
.connect_visible_notify(|obj: &super::MusicusWorkSelectorPopover| {
|
||||
if obj.is_visible() {
|
||||
obj.imp().stack.set_visible_child(&*obj.imp().composer_view);
|
||||
obj.imp().composer_search_entry.set_text("");
|
||||
obj.imp().composer_search_entry.grab_focus();
|
||||
obj.imp()
|
||||
.composer_scrolled_window
|
||||
.vadjustment()
|
||||
.set_value(0.0);
|
||||
}
|
||||
});
|
||||
|
||||
self.obj().search_composers("");
|
||||
}
|
||||
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![
|
||||
Signal::builder("selected")
|
||||
.param_types([Work::static_type()])
|
||||
.build(),
|
||||
Signal::builder("create").build(),
|
||||
]
|
||||
});
|
||||
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusWorkSelectorPopover {
|
||||
// TODO: Fix focus.
|
||||
fn focus(&self, direction_type: gtk::DirectionType) -> bool {
|
||||
if direction_type == gtk::DirectionType::Down {
|
||||
if self.stack.visible_child() == Some(self.composer_list.get().upcast()) {
|
||||
self.composer_list.child_focus(direction_type)
|
||||
} else {
|
||||
self.work_list.child_focus(direction_type)
|
||||
}
|
||||
} else {
|
||||
self.parent_focus(direction_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PopoverImpl for MusicusWorkSelectorPopover {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MusicusWorkSelectorPopover(ObjectSubclass<imp::MusicusWorkSelectorPopover>)
|
||||
@extends gtk::Widget, gtk::Popover;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MusicusWorkSelectorPopover {
|
||||
pub fn new(library: &MusicusLibrary) -> Self {
|
||||
glib::Object::builder().property("library", library).build()
|
||||
}
|
||||
|
||||
pub fn connect_selected<F: Fn(&Self, Work) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.connect_local("selected", true, move |values| {
|
||||
let obj = values[0].get::<Self>().unwrap();
|
||||
let work = values[1].get::<Work>().unwrap();
|
||||
f(&obj, work);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
fn composer_search_changed(&self, entry: >k::SearchEntry) {
|
||||
self.search_composers(&entry.text());
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn composer_activate(&self, _: >k::SearchEntry) {
|
||||
if let Some(composer) = self.imp().composers.borrow().first() {
|
||||
self.select_composer(composer.to_owned());
|
||||
} else {
|
||||
self.create();
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn back_button_clicked(&self, _: >k::Button) {
|
||||
self.imp()
|
||||
.stack
|
||||
.set_visible_child(&*self.imp().composer_view);
|
||||
self.imp().composer_search_entry.grab_focus();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn work_search_changed(&self, entry: >k::SearchEntry) {
|
||||
self.search_works(&entry.text());
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn work_activate(&self, _: >k::SearchEntry) {
|
||||
if let Some(work) = self.imp().works.borrow().first() {
|
||||
self.select(work.clone());
|
||||
} else {
|
||||
self.create();
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn stop_search(&self, _: >k::SearchEntry) {
|
||||
self.popdown();
|
||||
}
|
||||
|
||||
fn search_composers(&self, search: &str) {
|
||||
let imp = self.imp();
|
||||
|
||||
let persons = imp.library.get().unwrap().search_persons(search).unwrap();
|
||||
|
||||
imp.composer_list.remove_all();
|
||||
|
||||
for person in &persons {
|
||||
let row = MusicusActivatableRow::new(
|
||||
>k::Label::builder()
|
||||
.label(person.to_string())
|
||||
.halign(gtk::Align::Start)
|
||||
.ellipsize(pango::EllipsizeMode::Middle)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let person = person.clone();
|
||||
let obj = self.clone();
|
||||
row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.select_composer(person.clone());
|
||||
});
|
||||
|
||||
imp.composer_list.append(&row);
|
||||
}
|
||||
|
||||
let create_box = gtk::Box::builder().spacing(12).build();
|
||||
create_box.append(>k::Image::builder().icon_name("list-add-symbolic").build());
|
||||
create_box.append(
|
||||
>k::Label::builder()
|
||||
.label(gettext("Create new work"))
|
||||
.halign(gtk::Align::Start)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let create_row = MusicusActivatableRow::new(&create_box);
|
||||
let obj = self.clone();
|
||||
create_row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.create();
|
||||
});
|
||||
|
||||
imp.composer_list.append(&create_row);
|
||||
|
||||
imp.composers.replace(persons);
|
||||
}
|
||||
|
||||
fn search_works(&self, search: &str) {
|
||||
let imp = self.imp();
|
||||
|
||||
let works = imp
|
||||
.library
|
||||
.get()
|
||||
.unwrap()
|
||||
.search_works(imp.composer.borrow().as_ref().unwrap(), search)
|
||||
.unwrap();
|
||||
|
||||
imp.work_list.remove_all();
|
||||
|
||||
for work in &works {
|
||||
let row = MusicusActivatableRow::new(
|
||||
>k::Label::builder()
|
||||
.label(work.name.get())
|
||||
.halign(gtk::Align::Start)
|
||||
.ellipsize(pango::EllipsizeMode::Middle)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let work = work.clone();
|
||||
let obj = self.clone();
|
||||
row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.select(work.clone());
|
||||
});
|
||||
|
||||
imp.work_list.append(&row);
|
||||
}
|
||||
|
||||
let create_box = gtk::Box::builder().spacing(12).build();
|
||||
create_box.append(>k::Image::builder().icon_name("list-add-symbolic").build());
|
||||
create_box.append(
|
||||
>k::Label::builder()
|
||||
.label(gettext("Create new work"))
|
||||
.halign(gtk::Align::Start)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let create_row = MusicusActivatableRow::new(&create_box);
|
||||
let obj = self.clone();
|
||||
create_row.connect_activated(move |_: &MusicusActivatableRow| {
|
||||
obj.create();
|
||||
});
|
||||
|
||||
imp.work_list.append(&create_row);
|
||||
|
||||
imp.works.replace(works);
|
||||
}
|
||||
|
||||
fn select_composer(&self, person: Person) {
|
||||
self.imp().composer_label.set_text(person.name.get());
|
||||
self.imp().work_search_entry.set_text("");
|
||||
self.imp().work_search_entry.grab_focus();
|
||||
self.imp().work_scrolled_window.vadjustment().set_value(0.0);
|
||||
self.imp().stack.set_visible_child(&*self.imp().work_view);
|
||||
|
||||
self.imp().composer.replace(Some(person.clone()));
|
||||
self.search_works("");
|
||||
}
|
||||
|
||||
fn select(&self, work: Work) {
|
||||
self.emit_by_name::<()>("selected", &[&work]);
|
||||
self.popdown();
|
||||
}
|
||||
|
||||
fn create(&self) {
|
||||
self.emit_by_name::<()>("create", &[]);
|
||||
self.popdown();
|
||||
}
|
||||
}
|
||||
317
src/library.rs
317
src/library.rs
|
|
@ -48,7 +48,7 @@ mod imp {
|
|||
|
||||
let db_path = PathBuf::from(&self.folder.get().unwrap()).join("musicus.db");
|
||||
let connection = db::connect(db_path.to_str().unwrap()).unwrap();
|
||||
self.connection.set(Some(connection));
|
||||
self.connection.replace(Some(connection));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ impl MusicusLibrary {
|
|||
.distinct()
|
||||
.load::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, &&self.folder(), connection))
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?;
|
||||
|
||||
let albums = albums::table
|
||||
|
|
@ -293,7 +293,7 @@ impl MusicusLibrary {
|
|||
.distinct()
|
||||
.load::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, &self.folder(), connection))
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?;
|
||||
|
||||
let albums = albums::table
|
||||
|
|
@ -336,7 +336,7 @@ impl MusicusLibrary {
|
|||
.distinct()
|
||||
.load::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, &self.folder(), connection))
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?;
|
||||
|
||||
LibraryResults {
|
||||
|
|
@ -363,7 +363,7 @@ impl MusicusLibrary {
|
|||
.distinct()
|
||||
.load::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, &self.folder(), connection))
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?;
|
||||
|
||||
LibraryResults {
|
||||
|
|
@ -378,7 +378,7 @@ impl MusicusLibrary {
|
|||
.filter(recordings::work_id.eq(&work.work_id))
|
||||
.load::<tables::Recording>(connection)?
|
||||
.into_iter()
|
||||
.map(|r| Recording::from_table(r, &self.folder(), connection))
|
||||
.map(|r| Recording::from_table(r, connection))
|
||||
.collect::<Result<Vec<Recording>>>()?;
|
||||
|
||||
LibraryResults {
|
||||
|
|
@ -479,7 +479,23 @@ impl MusicusLibrary {
|
|||
.select(tables::Recording::as_select())
|
||||
.first::<tables::Recording>(connection)?;
|
||||
|
||||
Recording::from_table(row, &self.folder(), connection)
|
||||
Recording::from_table(row, connection)
|
||||
}
|
||||
|
||||
pub fn tracks_for_recording(&self, recording_id: &str) -> Result<Vec<Track>> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
let tracks = tracks::table
|
||||
.order(tracks::recording_index)
|
||||
.filter(tracks::recording_id.eq(&recording_id))
|
||||
.select(tables::Track::as_select())
|
||||
.load::<tables::Track>(connection)?
|
||||
.into_iter()
|
||||
.map(|t| Track::from_table(t, connection))
|
||||
.collect::<Result<Vec<Track>>>()?;
|
||||
|
||||
Ok(tracks)
|
||||
}
|
||||
|
||||
pub fn track_played(&self, track_id: &str) -> Result<()> {
|
||||
|
|
@ -572,6 +588,29 @@ impl MusicusLibrary {
|
|||
Ok(works)
|
||||
}
|
||||
|
||||
pub fn search_ensembles(&self, search: &str) -> Result<Vec<Ensemble>> {
|
||||
let search = format!("%{}%", search);
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
let ensembles = ensembles::table
|
||||
.order(ensembles::last_used_at.desc())
|
||||
.left_join(ensemble_persons::table.inner_join(persons::table))
|
||||
.filter(
|
||||
ensembles::name
|
||||
.like(&search)
|
||||
.or(persons::name.like(&search)),
|
||||
)
|
||||
.limit(20)
|
||||
.select(ensembles::all_columns)
|
||||
.load::<tables::Ensemble>(connection)?
|
||||
.into_iter()
|
||||
.map(|e| Ensemble::from_table(e, connection))
|
||||
.collect::<Result<Vec<Ensemble>>>()?;
|
||||
|
||||
Ok(ensembles)
|
||||
}
|
||||
|
||||
pub fn composer_default_role(&self) -> Result<Role> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
|
@ -581,6 +620,15 @@ impl MusicusLibrary {
|
|||
.first::<Role>(connection)?)
|
||||
}
|
||||
|
||||
pub fn performer_default_role(&self) -> Result<Role> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
Ok(roles::table
|
||||
.filter(roles::role_id.eq("28ff0aeb11c041a6916d93e9b4884eef"))
|
||||
.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();
|
||||
|
|
@ -621,6 +669,46 @@ impl MusicusLibrary {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_instrument(&self, name: TranslatedString) -> Result<Instrument> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
let now = Local::now().naive_local();
|
||||
|
||||
let instrument = Instrument {
|
||||
instrument_id: db::generate_id(),
|
||||
name,
|
||||
created_at: now,
|
||||
edited_at: now,
|
||||
last_used_at: now,
|
||||
last_played_at: None,
|
||||
};
|
||||
|
||||
diesel::insert_into(instruments::table)
|
||||
.values(&instrument)
|
||||
.execute(connection)?;
|
||||
|
||||
Ok(instrument)
|
||||
}
|
||||
|
||||
pub fn update_instrument(&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(instruments::table)
|
||||
.filter(instruments::instrument_id.eq(id))
|
||||
.set((
|
||||
instruments::name.eq(name),
|
||||
instruments::edited_at.eq(now),
|
||||
instruments::last_used_at.eq(now),
|
||||
))
|
||||
.execute(connection)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_role(&self, name: TranslatedString) -> Result<Role> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
|
@ -659,6 +747,221 @@ impl MusicusLibrary {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_work(
|
||||
&self,
|
||||
name: TranslatedString,
|
||||
parts: Vec<WorkPart>,
|
||||
persons: Vec<Composer>,
|
||||
instruments: Vec<Instrument>,
|
||||
) -> Result<Work> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
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,
|
||||
name,
|
||||
created_at: now,
|
||||
edited_at: now,
|
||||
last_used_at: now,
|
||||
last_played_at: None,
|
||||
};
|
||||
|
||||
diesel::insert_into(works::table)
|
||||
.values(&work_data)
|
||||
.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)?;
|
||||
}
|
||||
|
||||
for (index, composer) in persons.into_iter().enumerate() {
|
||||
let composer_data = tables::WorkPerson {
|
||||
work_id: work_id.clone(),
|
||||
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)?;
|
||||
}
|
||||
|
||||
for (index, instrument) in instruments.into_iter().enumerate() {
|
||||
let instrument_data = tables::WorkInstrument {
|
||||
work_id: work_id.clone(),
|
||||
instrument_id: instrument.instrument_id,
|
||||
sequence_number: index as i32,
|
||||
};
|
||||
|
||||
diesel::insert_into(work_instruments::table)
|
||||
.values(instrument_data)
|
||||
.execute(connection)?;
|
||||
}
|
||||
|
||||
let work = Work::from_table(work_data, connection)?;
|
||||
|
||||
Ok(work)
|
||||
}
|
||||
|
||||
pub fn update_work(
|
||||
&self,
|
||||
id: &str,
|
||||
name: TranslatedString,
|
||||
parts: Vec<WorkPart>,
|
||||
persons: Vec<Composer>,
|
||||
instruments: Vec<Instrument>,
|
||||
) -> Result<()> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
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!()
|
||||
}
|
||||
|
||||
pub fn create_ensemble(&self, name: TranslatedString) -> Result<Ensemble> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
let now = Local::now().naive_local();
|
||||
|
||||
let ensemble_data = tables::Ensemble {
|
||||
ensemble_id: db::generate_id(),
|
||||
name,
|
||||
created_at: now,
|
||||
edited_at: now,
|
||||
last_used_at: now,
|
||||
last_played_at: None,
|
||||
};
|
||||
|
||||
// TODO: Add persons.
|
||||
|
||||
diesel::insert_into(ensembles::table)
|
||||
.values(&ensemble_data)
|
||||
.execute(connection)?;
|
||||
|
||||
let ensemble = Ensemble::from_table(ensemble_data, connection)?;
|
||||
|
||||
Ok(ensemble)
|
||||
}
|
||||
|
||||
pub fn update_ensemble(&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(ensembles::table)
|
||||
.filter(ensembles::ensemble_id.eq(id))
|
||||
.set((
|
||||
ensembles::name.eq(name),
|
||||
ensembles::edited_at.eq(now),
|
||||
ensembles::last_used_at.eq(now),
|
||||
))
|
||||
.execute(connection)?;
|
||||
|
||||
// TODO: Support updating persons.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_recording(
|
||||
&self,
|
||||
work: Work,
|
||||
year: Option<i32>,
|
||||
performers: Vec<Performer>,
|
||||
ensembles: Vec<EnsemblePerformer>,
|
||||
) -> Result<Recording> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
let recording_id = db::generate_id();
|
||||
let now = Local::now().naive_local();
|
||||
|
||||
let recording_data = tables::Recording {
|
||||
recording_id: recording_id.clone(),
|
||||
work_id: work.work_id.clone(),
|
||||
year,
|
||||
created_at: now,
|
||||
edited_at: now,
|
||||
last_used_at: now,
|
||||
last_played_at: None,
|
||||
};
|
||||
|
||||
diesel::insert_into(recordings::table)
|
||||
.values(&recording_data)
|
||||
.execute(connection)?;
|
||||
|
||||
for (index, performer) in performers.into_iter().enumerate() {
|
||||
let recording_person_data = tables::RecordingPerson {
|
||||
recording_id: recording_id.clone(),
|
||||
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)?;
|
||||
}
|
||||
|
||||
for (index, ensemble) in ensembles.into_iter().enumerate() {
|
||||
let recording_ensemble_data = tables::RecordingEnsemble {
|
||||
recording_id: recording_id.clone(),
|
||||
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)?;
|
||||
}
|
||||
|
||||
let recording = Recording::from_table(recording_data, connection)?;
|
||||
|
||||
Ok(recording)
|
||||
}
|
||||
|
||||
pub fn update_recording(
|
||||
&self,
|
||||
id: &str,
|
||||
work: Work,
|
||||
year: Option<i32>,
|
||||
performers: Vec<Performer>,
|
||||
ensembles: Vec<EnsemblePerformer>,
|
||||
) -> Result<()> {
|
||||
let mut binding = self.imp().connection.borrow_mut();
|
||||
let connection = &mut *binding.as_mut().unwrap();
|
||||
|
||||
let now = Local::now().naive_local();
|
||||
|
||||
// TODO: Update recording.
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
use adw::{
|
||||
prelude::*,
|
||||
subclass::{navigation_page::NavigationPageImpl, prelude::*},
|
||||
};
|
||||
use gtk::glib::{self, Properties};
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib;
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use crate::library::MusicusLibrary;
|
||||
use crate::{
|
||||
editor::{
|
||||
ensemble_editor::MusicusEnsembleEditor, instrument_editor::MusicusInstrumentEditor,
|
||||
person_editor::MusicusPersonEditor, recording_editor::MusicusRecordingEditor,
|
||||
role_editor::MusicusRoleEditor, work_editor::MusicusWorkEditor,
|
||||
},
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
|
||||
#[properties(wrapper_type = super::LibraryManager)]
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||
#[template(file = "data/ui/library_manager.blp")]
|
||||
pub struct LibraryManager {
|
||||
#[property(get, construct_only)]
|
||||
pub navigation: OnceCell<adw::NavigationView>,
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
}
|
||||
|
||||
|
|
@ -34,9 +37,7 @@ mod imp {
|
|||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for LibraryManager {}
|
||||
|
||||
impl WidgetImpl for LibraryManager {}
|
||||
impl NavigationPageImpl for LibraryManager {}
|
||||
}
|
||||
|
|
@ -48,7 +49,110 @@ glib::wrapper! {
|
|||
|
||||
#[gtk::template_callbacks]
|
||||
impl LibraryManager {
|
||||
pub fn new(library: &MusicusLibrary) -> Self {
|
||||
glib::Object::builder().property("library", library).build()
|
||||
pub fn new(navigation: &adw::NavigationView, library: &MusicusLibrary) -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
let imp = obj.imp();
|
||||
|
||||
imp.navigation.set(navigation.to_owned()).unwrap();
|
||||
imp.library.set(library.to_owned()).unwrap();
|
||||
|
||||
obj
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_person(&self, _: >k::Button) {
|
||||
self.imp()
|
||||
.navigation
|
||||
.get()
|
||||
.unwrap()
|
||||
.push(&MusicusPersonEditor::new(
|
||||
&self.imp().navigation.get().unwrap(),
|
||||
&self.imp().library.get().unwrap(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_role(&self, _: >k::Button) {
|
||||
self.imp()
|
||||
.navigation
|
||||
.get()
|
||||
.unwrap()
|
||||
.push(&MusicusRoleEditor::new(
|
||||
&self.imp().navigation.get().unwrap(),
|
||||
&self.imp().library.get().unwrap(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_instrument(&self, _: >k::Button) {
|
||||
self.imp()
|
||||
.navigation
|
||||
.get()
|
||||
.unwrap()
|
||||
.push(&MusicusInstrumentEditor::new(
|
||||
&self.imp().navigation.get().unwrap(),
|
||||
&self.imp().library.get().unwrap(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_work(&self, _: >k::Button) {
|
||||
self.imp()
|
||||
.navigation
|
||||
.get()
|
||||
.unwrap()
|
||||
.push(&MusicusWorkEditor::new(
|
||||
&self.imp().navigation.get().unwrap(),
|
||||
&self.imp().library.get().unwrap(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_ensemble(&self, _: >k::Button) {
|
||||
self.imp()
|
||||
.navigation
|
||||
.get()
|
||||
.unwrap()
|
||||
.push(&MusicusEnsembleEditor::new(
|
||||
&self.imp().navigation.get().unwrap(),
|
||||
&self.imp().library.get().unwrap(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_recording(&self, _: >k::Button) {
|
||||
self.imp()
|
||||
.navigation
|
||||
.get()
|
||||
.unwrap()
|
||||
.push(&MusicusRecordingEditor::new(
|
||||
&self.imp().navigation.get().unwrap(),
|
||||
&self.imp().library.get().unwrap(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_medium(&self, _: >k::Button) {
|
||||
todo!("Medium import");
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn add_album(&self, _: >k::Button) {
|
||||
todo!("Album editor");
|
||||
// self.imp()
|
||||
// .navigation
|
||||
// .get()
|
||||
// .unwrap()
|
||||
// .push(&MusicusAlbumEditor::new(
|
||||
// &self.imp().navigation.get().unwrap(),
|
||||
// &self.imp().library.get().unwrap(),
|
||||
// None,
|
||||
// ));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{
|
||||
cell::{Cell, OnceCell, RefCell},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
|
|
@ -236,7 +237,7 @@ impl MusicusPlayer {
|
|||
}
|
||||
|
||||
pub fn play_recording(&self, recording: &Recording) {
|
||||
let tracks = &recording.tracks;
|
||||
let tracks = &self.library().unwrap().tracks_for_recording(&recording.recording_id).unwrap();
|
||||
|
||||
if tracks.is_empty() {
|
||||
log::warn!("Ignoring recording without tracks being added to the playlist.");
|
||||
|
|
@ -254,7 +255,7 @@ impl MusicusPlayer {
|
|||
&recording.work.name.get(),
|
||||
Some(&performances),
|
||||
None,
|
||||
&tracks[0].path,
|
||||
&self.library_path_to_file_path(&tracks[0].path),
|
||||
&tracks[0].track_id,
|
||||
));
|
||||
} else {
|
||||
|
|
@ -282,7 +283,7 @@ impl MusicusPlayer {
|
|||
&recording.work.name.get(),
|
||||
Some(&performances),
|
||||
Some(&track_title(&first_track, 1)),
|
||||
&first_track.path,
|
||||
&self.library_path_to_file_path(&first_track.path),
|
||||
&first_track.track_id,
|
||||
));
|
||||
|
||||
|
|
@ -294,7 +295,7 @@ impl MusicusPlayer {
|
|||
Some(&performances),
|
||||
// track number = track index + 1 (first track) + 1 (zero based)
|
||||
Some(&track_title(&track, index + 2)),
|
||||
&track.path,
|
||||
&self.library_path_to_file_path(&track.path),
|
||||
&track.track_id,
|
||||
));
|
||||
}
|
||||
|
|
@ -384,6 +385,14 @@ impl MusicusPlayer {
|
|||
self.play_recording(&recording);
|
||||
}
|
||||
}
|
||||
|
||||
fn library_path_to_file_path(&self, path: &str) -> String {
|
||||
PathBuf::from(self.library().unwrap().folder())
|
||||
.join(path)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MusicusPlayer {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
use gtk::{glib, subclass::prelude::*};
|
||||
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use crate::db::models::Recording;
|
||||
use crate::{
|
||||
db::models::Recording, editor::recording_editor::MusicusRecordingEditor,
|
||||
library::MusicusLibrary,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
|
@ -16,6 +19,8 @@ mod imp {
|
|||
#[template_child]
|
||||
pub performances_label: TemplateChild<gtk::Label>,
|
||||
|
||||
pub navigation: OnceCell<adw::NavigationView>,
|
||||
pub library: OnceCell<MusicusLibrary>,
|
||||
pub recording: OnceCell<Recording>,
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +39,31 @@ mod imp {
|
|||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for MusicusRecordingTile {}
|
||||
impl ObjectImpl for MusicusRecordingTile {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj().to_owned();
|
||||
let edit_action = gio::ActionEntry::builder("edit")
|
||||
.activate(move |_, _, _| {
|
||||
obj.imp()
|
||||
.navigation
|
||||
.get()
|
||||
.unwrap()
|
||||
.push(&MusicusRecordingEditor::new(
|
||||
obj.imp().navigation.get().unwrap(),
|
||||
obj.imp().library.get().unwrap(),
|
||||
Some(&obj.imp().recording.get().unwrap()),
|
||||
));
|
||||
})
|
||||
.build();
|
||||
|
||||
let actions = gio::SimpleActionGroup::new();
|
||||
actions.add_action_entries([edit_action]);
|
||||
self.obj().insert_action_group("recording", Some(&actions));
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MusicusRecordingTile {}
|
||||
impl FlowBoxChildImpl for MusicusRecordingTile {}
|
||||
}
|
||||
|
|
@ -45,15 +74,23 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl MusicusRecordingTile {
|
||||
pub fn new(recording: &Recording) -> Self {
|
||||
pub fn new(
|
||||
navigation: &adw::NavigationView,
|
||||
library: &MusicusLibrary,
|
||||
recording: &Recording,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
let imp = obj.imp();
|
||||
|
||||
imp.work_label.set_label(&recording.work.name.get());
|
||||
imp.composer_label.set_label(&recording.work.composers_string());
|
||||
imp.performances_label.set_label(&recording.performers_string());
|
||||
imp.composer_label
|
||||
.set_label(&recording.work.composers_string());
|
||||
imp.performances_label
|
||||
.set_label(&recording.performers_string());
|
||||
|
||||
imp.recording.set(recording.clone()).unwrap();
|
||||
imp.navigation.set(navigation.to_owned()).unwrap();
|
||||
imp.library.set(library.to_owned()).unwrap();
|
||||
imp.recording.set(recording.to_owned()).unwrap();
|
||||
|
||||
obj
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,6 @@ impl MusicusWindow {
|
|||
let navigation = self.imp().navigation_view.get();
|
||||
navigation
|
||||
.replace(&[MusicusHomePage::new(&navigation, &library, &self.imp().player).into()]);
|
||||
navigation.add(&LibraryManager::new(&library));
|
||||
navigation.add(&LibraryManager::new(&navigation, &library));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue