Add work and recording editor

This commit is contained in:
Elias Projahn 2025-01-15 11:23:04 +01:00
parent 36b2f1097e
commit 364557d959
30 changed files with 3308 additions and 418 deletions

609
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
using Gtk 4.0;
using Adw 1;
template $MusicusEnsembleEditor: Adw.NavigationPage {
title: _("Ensemble");
Adw.ToolbarView {
[top]
Adw.HeaderBar header_bar {}
Adw.Clamp {
Gtk.Box {
orientation: vertical;
Gtk.Label {
label: _("Name");
xalign: 0;
margin-top: 24;
styles [
"heading"
]
}
$MusicusTranslationEditor name_editor {
margin-top: 12;
}
Gtk.Button save_button {
margin-top: 24;
label: _("Create ensemble");
clicked => $save() swapped;
styles [
"card",
"save"
]
}
}
}
}
}

View file

@ -0,0 +1,35 @@
using Gtk 4.0;
using Adw 1;
template $MusicusEnsembleSelectorPopover: Gtk.Popover {
styles [
"selector"
]
Adw.ToolbarView {
[top]
Gtk.SearchEntry search_entry {
placeholder-text: _("Search ensembles…");
margin-start: 8;
margin-end: 8;
margin-top: 8;
margin-bottom: 6;
search-changed => $search_changed() swapped;
activate => $activate() swapped;
stop-search => $stop_search() swapped;
}
Gtk.ScrolledWindow scrolled_window {
height-request: 200;
Gtk.ListBox list_box {
styles [
"selector-list"
]
selection-mode: none;
activate-on-single-click: true;
}
}
}
}

View file

@ -0,0 +1,42 @@
using Gtk 4.0;
using Adw 1;
template $MusicusInstrumentEditor: Adw.NavigationPage {
title: _("Instrument");
Adw.ToolbarView {
[top]
Adw.HeaderBar header_bar {}
Adw.Clamp {
Gtk.Box {
orientation: vertical;
Gtk.Label {
label: _("Name");
xalign: 0;
margin-top: 24;
styles [
"heading"
]
}
$MusicusTranslationEditor name_editor {
margin-top: 12;
}
Gtk.Button save_button {
margin-top: 24;
label: _("Create instrument");
clicked => $save() swapped;
styles [
"card",
"save"
]
}
}
}
}
}

View file

@ -8,5 +8,50 @@ template $MusicusLibraryManager : Adw.NavigationPage {
Adw.ToolbarView {
[top]
Adw.HeaderBar {}
Gtk.Box {
orientation: vertical;
spacing: 12;
Gtk.Button {
label: _("Add person");
clicked => $add_person() swapped;
}
Gtk.Button {
label: _("Add role");
clicked => $add_role() swapped;
}
Gtk.Button {
label: _("Add instrument");
clicked => $add_instrument() swapped;
}
Gtk.Button {
label: _("Add work");
clicked => $add_work() swapped;
}
Gtk.Button {
label: _("Add ensemble");
clicked => $add_ensemble() swapped;
}
Gtk.Button {
label: _("Add recording");
clicked => $add_recording() swapped;
}
Gtk.Button {
label: _("Add album");
clicked => $add_album() swapped;
}
Gtk.Button {
label: _("Add medium");
clicked => $add_medium() swapped;
}
}
}
}

View file

@ -0,0 +1,94 @@
using Gtk 4.0;
using Adw 1;
template $MusicusPerformerRoleSelectorPopover: Gtk.Popover {
styles [
"selector"
]
Gtk.Stack stack {
transition-type: slide_left_right;
Adw.ToolbarView role_view {
[top]
Gtk.SearchEntry role_search_entry {
placeholder-text: _("Search roles…");
margin-start: 8;
margin-end: 8;
margin-top: 8;
margin-bottom: 6;
search-changed => $role_search_changed() swapped;
activate => $role_activate() swapped;
stop-search => $stop_search() swapped;
}
Gtk.ScrolledWindow role_scrolled_window {
height-request: 200;
Gtk.ListBox role_list {
styles [
"selector-list"
]
selection-mode: none;
activate-on-single-click: true;
}
}
}
Adw.ToolbarView instrument_view {
[top]
Gtk.Box {
margin-start: 8;
margin-end: 8;
margin-top: 8;
margin-bottom: 6;
orientation: vertical;
Gtk.CenterBox {
[start]
Gtk.Button {
styles [
"flat"
]
icon-name: "go-previous-symbolic";
clicked => $back_button_clicked() swapped;
}
[center]
Gtk.Label {
styles [
"heading"
]
label: _("Performer");
ellipsize: end;
margin-start: 6;
}
}
Gtk.SearchEntry instrument_search_entry {
placeholder-text: _("Search instruments…");
margin-top: 6;
search-changed => $instrument_search_changed() swapped;
activate => $instrument_activate() swapped;
stop-search => $stop_search() swapped;
}
}
Gtk.ScrolledWindow instrument_scrolled_window {
height-request: 200;
Gtk.ListBox instrument_list {
styles [
"selector-list"
]
selection-mode: none;
activate-on-single-click: true;
}
}
}
}
}

View file

@ -0,0 +1,141 @@
using Gtk 4.0;
using Adw 1;
template $MusicusRecordingEditor: Adw.NavigationPage {
title: _("Recording");
Adw.ToolbarView {
[top]
Adw.HeaderBar header_bar {}
Gtk.ScrolledWindow {
Adw.Clamp {
Gtk.Box {
orientation: vertical;
margin-bottom: 24;
margin-start: 12;
margin-end: 12;
Gtk.Label {
label: _("Recording");
xalign: 0;
margin-top: 24;
styles [
"heading"
]
}
Gtk.ListBox {
selection-mode: none;
margin-top: 12;
styles [
"boxed-list"
]
Adw.ActionRow work_row {
title: _("Select work");
activatable: true;
activated => $select_work() swapped;
[prefix]
Gtk.Box select_work_box {
Gtk.Image {
icon-name: "document-edit-symbolic";
}
}
}
Adw.SpinRow year_row {
title: _("Year");
adjustment: Gtk.Adjustment {
lower: 0;
upper: 3000;
value: 2000;
step-increment: 1;
page-increment: 10;
};
}
}
Gtk.Label {
label: _("Performers");
xalign: 0;
margin-top: 24;
styles [
"heading"
]
}
Gtk.ListBox performer_list {
selection-mode: none;
margin-top: 12;
styles [
"boxed-list"
]
Adw.ActionRow {
title: _("Add performer");
activatable: true;
activated => $select_person() swapped;
[prefix]
Gtk.Box select_person_box {
Gtk.Image {
icon-name: "list-add-symbolic";
}
}
}
}
Gtk.Label {
label: _("Ensembles");
xalign: 0;
margin-top: 24;
styles [
"heading"
]
}
Gtk.ListBox ensemble_list {
selection-mode: none;
margin-top: 12;
styles [
"boxed-list"
]
Adw.ActionRow {
title: _("Add ensemble");
activatable: true;
activated => $select_ensemble() swapped;
[prefix]
Gtk.Box select_ensemble_box {
Gtk.Image {
icon-name: "list-add-symbolic";
}
}
}
}
Gtk.Button save_button {
margin-top: 24;
label: _("Create recording");
clicked => $save() swapped;
styles [
"card",
"save"
]
}
}
}
}
}
}

View file

@ -0,0 +1,33 @@
using Gtk 4.0;
using Adw 1;
template $MusicusRecordingEditorEnsembleRow: Adw.ActionRow {
Gtk.Button {
icon-name: "user-trash-symbolic";
valign: center;
clicked => $remove() swapped;
styles [
"flat"
]
}
Gtk.Button {
valign: center;
clicked => $open_role_popover() swapped;
styles [
"flat"
]
Gtk.Box role_box {
spacing: 6;
Gtk.Label role_label {}
Gtk.Image {
icon-name: "pan-down-symbolic";
}
}
}
}

View file

@ -0,0 +1,33 @@
using Gtk 4.0;
using Adw 1;
template $MusicusRecordingEditorPerformerRow: Adw.ActionRow {
Gtk.Button {
icon-name: "user-trash-symbolic";
valign: center;
clicked => $remove() swapped;
styles [
"flat"
]
}
Gtk.Button {
valign: center;
clicked => $open_role_popover() swapped;
styles [
"flat"
]
Gtk.Box role_box {
spacing: 6;
Gtk.Label role_label {}
Gtk.Image {
icon-name: "pan-down-symbolic";
}
}
}
}

View file

@ -1,8 +1,11 @@
using Gtk 4.0;
using Adw 1;
template $MusicusRecordingTile: Gtk.FlowBoxChild {
styles ["card", "activatable", "tile"]
styles [
"card",
"activatable",
"tile"
]
Gtk.Box {
spacing: 12;
@ -18,29 +21,50 @@ template $MusicusRecordingTile : Gtk.FlowBoxChild {
hexpand: true;
Gtk.Label work_label {
styles ["work"]
styles [
"work"
]
halign: start;
wrap: true;
}
Gtk.Label composer_label {
styles ["composer"]
styles [
"composer"
]
halign: start;
wrap: true;
}
Gtk.Label performances_label {
styles ["performances", "dim-label"]
styles [
"performances",
"dim-label"
]
halign: start;
wrap: true;
}
}
Gtk.Button {
styles ["flat"]
Gtk.MenuButton {
styles [
"flat"
]
valign: start;
margin-top: 12;
icon-name: "view-more-symbolic";
popover: Gtk.PopoverMenu {
menu-model: edit_menu;
};
}
}
}
menu edit_menu {
item (_("Edit recording"), "recording.edit")
}

View file

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

View file

@ -0,0 +1,93 @@
using Gtk 4.0;
using Adw 1;
template $MusicusWorkSelectorPopover: Gtk.Popover {
styles [
"selector"
]
Gtk.Stack stack {
transition-type: slide_left_right;
Adw.ToolbarView composer_view {
[top]
Gtk.SearchEntry composer_search_entry {
placeholder-text: _("Search composers…");
margin-start: 8;
margin-end: 8;
margin-top: 8;
margin-bottom: 6;
search-changed => $composer_search_changed() swapped;
activate => $composer_activate() swapped;
stop-search => $stop_search() swapped;
}
Gtk.ScrolledWindow composer_scrolled_window {
height-request: 200;
Gtk.ListBox composer_list {
styles [
"selector-list"
]
selection-mode: none;
activate-on-single-click: true;
}
}
}
Adw.ToolbarView work_view {
[top]
Gtk.Box {
margin-start: 8;
margin-end: 8;
margin-top: 8;
margin-bottom: 6;
orientation: vertical;
Gtk.CenterBox {
[start]
Gtk.Button {
styles [
"flat"
]
icon-name: "go-previous-symbolic";
clicked => $back_button_clicked() swapped;
}
[center]
Gtk.Label composer_label {
styles [
"heading"
]
ellipsize: end;
margin-start: 6;
}
}
Gtk.SearchEntry work_search_entry {
placeholder-text: _("Search works…");
margin-top: 6;
search-changed => $work_search_changed() swapped;
activate => $work_activate() swapped;
stop-search => $stop_search() swapped;
}
}
Gtk.ScrolledWindow work_scrolled_window {
height-request: 200;
Gtk.ListBox work_list {
styles [
"selector-list"
]
selection-mode: none;
activate-on-single-click: true;
}
}
}
}
}

View file

@ -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,
})
}

View file

@ -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,

View 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, _: &gtk::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();
}
}

View 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: &gtk::SearchEntry) {
self.search(&entry.text());
}
#[template_callback]
fn activate(&self, _: &gtk::SearchEntry) {
if let Some(ensemble) = self.imp().ensembles.borrow().first() {
self.select(ensemble.clone());
} else {
self.create();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::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(
&gtk::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(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::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();
}
}

View 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, _: &gtk::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();
}
}

View file

@ -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_selector_popover;

View 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: &gtk::SearchEntry) {
self.search_roles(&entry.text());
}
#[template_callback]
fn role_activate(&self, _: &gtk::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, _: &gtk::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: &gtk::SearchEntry) {
self.search_instruments(&entry.text());
}
#[template_callback]
fn instrument_activate(&self, _: &gtk::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, _: &gtk::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(
&gtk::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(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::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(
&gtk::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(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::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();
}
}

View 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, _: &gtk::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();
}
}

View 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, _: &gtk::Button) {
self.imp().role_popover.get().unwrap().popup();
}
#[template_callback]
fn remove(&self, _: &gtk::Button) {
self.emit_by_name::<()>("remove", &[]);
}
}

View 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, _: &gtk::Button) {
self.imp().role_popover.get().unwrap().popup();
}
#[template_callback]
fn remove(&self, _: &gtk::Button) {
self.emit_by_name::<()>("remove", &[]);
}
}

View file

@ -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!(
editor.connect_created(clone!(
#[weak]
obj,
#[weak]
row,
#[strong]
instrument,
move |_| {
obj.imp().instrument_list.remove(&row);
obj.imp()
.instruments
.borrow_mut()
.retain(|i| *i != instrument);
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, _: &gtk::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();
}
}

View file

@ -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, _: &gtk::Button) {
self.emit_by_name::<()>("remove", &[]);

View 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: &gtk::SearchEntry) {
self.search_composers(&entry.text());
}
#[template_callback]
fn composer_activate(&self, _: &gtk::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, _: &gtk::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: &gtk::SearchEntry) {
self.search_works(&entry.text());
}
#[template_callback]
fn work_activate(&self, _: &gtk::SearchEntry) {
if let Some(work) = self.imp().works.borrow().first() {
self.select(work.clone());
} else {
self.create();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::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(
&gtk::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(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::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(
&gtk::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(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::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();
}
}

View file

@ -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)]

View file

@ -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, _: &gtk::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, _: &gtk::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, _: &gtk::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, _: &gtk::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, _: &gtk::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, _: &gtk::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, _: &gtk::Button) {
todo!("Medium import");
}
#[template_callback]
fn add_album(&self, _: &gtk::Button) {
todo!("Album editor");
// self.imp()
// .navigation
// .get()
// .unwrap()
// .push(&MusicusAlbumEditor::new(
// &self.imp().navigation.get().unwrap(),
// &self.imp().library.get().unwrap(),
// None,
// ));
}
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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));
}
}