Add tracks editor UI

This commit is contained in:
Elias Projahn 2025-02-09 10:00:46 +01:00
parent 0fe143a383
commit 143876c4de
12 changed files with 1159 additions and 26 deletions

7
Cargo.lock generated
View file

@ -552,6 +552,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "formatx"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa6f3b9014e23925937fbf4d05f27a6f4efe42545f98690b94f193bdb3f1959e"
[[package]] [[package]]
name = "fragile" name = "fragile"
version = "2.0.0" version = "2.0.0"
@ -1292,6 +1298,7 @@ dependencies = [
"chrono", "chrono",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"formatx",
"fragile", "fragile",
"gettext-rs", "gettext-rs",
"glib", "glib",

View file

@ -9,6 +9,7 @@ anyhow = "1"
chrono = "0.4" chrono = "0.4"
diesel = { version = "2.2", features = ["chrono", "sqlite"] } diesel = { version = "2.2", features = ["chrono", "sqlite"] }
diesel_migrations = "2.2" diesel_migrations = "2.2"
formatx = "0.2"
fragile = "2" fragile = "2"
gettext-rs = { version = "0.7", features = ["gettext-system"] } gettext-rs = { version = "0.7", features = ["gettext-system"] }
gstreamer-play = "0.23" gstreamer-play = "0.23"

View file

@ -258,6 +258,11 @@ template $MusicusHomePage: Adw.NavigationPage {
} }
menu primary_menu { menu primary_menu {
item {
label: _("_Import music");
action: "win.import";
}
item { item {
label: _("_Library manager"); label: _("_Library manager");
action: "win.library"; action: "win.library";

View file

@ -0,0 +1,147 @@
using Gtk 4.0;
using Adw 1;
template $MusicusRecordingSelectorPopover: 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_to_composer() 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;
}
}
}
Adw.ToolbarView recording_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_to_work() swapped;
}
[center]
Gtk.Label work_label {
styles [
"heading"
]
ellipsize: end;
margin-start: 6;
}
}
Gtk.SearchEntry recording_search_entry {
placeholder-text: _("Search recordings…");
margin-top: 6;
search-changed => $recording_search_changed() swapped;
activate => $recording_activate() swapped;
stop-search => $stop_search() swapped;
}
}
Gtk.ScrolledWindow recording_scrolled_window {
height-request: 200;
Gtk.ListBox recording_list {
styles [
"selector-list"
]
selection-mode: none;
activate-on-single-click: true;
}
}
}
}
}

98
data/ui/tracks_editor.blp Normal file
View file

@ -0,0 +1,98 @@
using Gtk 4.0;
using Adw 1;
template $MusicusTracksEditor: Adw.NavigationPage {
title: _("Tracks");
Adw.ToolbarView {
[top]
Adw.HeaderBar {}
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 recording_row {
title: _("Select recording");
activatable: true;
activated => $select_recording() swapped;
[prefix]
Gtk.Box select_recording_box {
Gtk.Image {
icon-name: "document-edit-symbolic";
}
}
}
}
Gtk.Label {
label: _("Tracks");
xalign: 0;
margin-top: 24;
styles [
"heading"
]
}
Gtk.ListBox track_list {
selection-mode: none;
margin-top: 12;
styles [
"boxed-list"
]
Adw.ActionRow {
title: _("Add files");
activatable: true;
activated => $add_files() swapped;
[prefix]
Gtk.Image {
icon-name: "list-add-symbolic";
}
}
}
Gtk.ListBox {
selection-mode: none;
margin-top: 24;
styles [
"boxed-list"
]
Adw.ButtonRow save_row {
title: _("Import tracks");
activated => $save() swapped;
}
}
}
}
}
}
}

View file

@ -0,0 +1,25 @@
using Gtk 4.0;
using Adw 1;
template $MusicusTracksEditorTrackRow: Adw.ActionRow {
title: _("Select parts");
activatable: true;
activated => $select_parts() swapped;
[prefix]
Gtk.Box select_parts_box {
Gtk.Image {
icon-name: "document-edit-symbolic";
}
}
Gtk.Button {
icon-name: "user-trash-symbolic";
valign: center;
clicked => $remove() swapped;
styles [
"flat"
]
}
}

View file

@ -9,8 +9,11 @@ pub mod person_selector_popover;
pub mod recording_editor; pub mod recording_editor;
pub mod recording_editor_ensemble_row; pub mod recording_editor_ensemble_row;
pub mod recording_editor_performer_row; pub mod recording_editor_performer_row;
pub mod recording_selector_popover;
pub mod role_editor; pub mod role_editor;
pub mod role_selector_popover; pub mod role_selector_popover;
pub mod tracks_editor;
pub mod tracks_editor_track_row;
pub mod translation_editor; pub mod translation_editor;
pub mod translation_entry; pub mod translation_entry;
pub mod work_editor; pub mod work_editor;

View file

@ -0,0 +1,424 @@
use crate::{
db::models::{Person, Recording, 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::RecordingSelectorPopover)]
#[template(file = "data/ui/recording_selector_popover.blp")]
pub struct RecordingSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub composers: RefCell<Vec<Person>>,
pub works: RefCell<Vec<Work>>,
pub recordings: RefCell<Vec<Recording>>,
pub composer: RefCell<Option<Person>>,
pub work: RefCell<Option<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>,
#[template_child]
pub recording_view: TemplateChild<adw::ToolbarView>,
#[template_child]
pub work_label: TemplateChild<gtk::Label>,
#[template_child]
pub recording_search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub recording_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub recording_list: TemplateChild<gtk::ListBox>,
}
#[glib::object_subclass]
impl ObjectSubclass for RecordingSelectorPopover {
const NAME: &'static str = "MusicusRecordingSelectorPopover";
type Type = super::RecordingSelectorPopover;
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 RecordingSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj()
.connect_visible_notify(|obj: &super::RecordingSelectorPopover| {
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([Recording::static_type()])
.build(),
Signal::builder("create").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for RecordingSelectorPopover {
// 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 if self.stack.visible_child() == Some(self.work_list.get().upcast()) {
self.work_list.child_focus(direction_type)
} else {
self.recording_list.child_focus(direction_type)
}
} else {
self.parent_focus(direction_type)
}
}
}
impl PopoverImpl for RecordingSelectorPopover {}
}
glib::wrapper! {
pub struct RecordingSelectorPopover(ObjectSubclass<imp::RecordingSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl RecordingSelectorPopover {
pub fn new(library: &MusicusLibrary) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_selected<F: Fn(&Self, Recording) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let recording = values[1].get::<Recording>().unwrap();
f(&obj, recording);
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_to_composer(&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(work.to_owned());
} else {
self.create();
}
}
#[template_callback]
fn back_to_work(&self, _: &gtk::Button) {
self.imp().stack.set_visible_child(&*self.imp().work_view);
self.imp().work_search_entry.grab_focus();
}
#[template_callback]
fn recording_search_changed(&self, entry: &gtk::SearchEntry) {
self.search_recordings(&entry.text());
}
#[template_callback]
fn recording_activate(&self, _: &gtk::SearchEntry) {
if let Some(recording) = self.imp().recordings.borrow().first() {
self.select(recording.to_owned());
} 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(),
);
row.set_tooltip_text(Some(&person.to_string()));
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 recording"))
.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(),
);
row.set_tooltip_text(Some(&work.name.get()));
let work = work.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
obj.select_work(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 recording"))
.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 search_recordings(&self, search: &str) {
let imp = self.imp();
let recordings = imp
.library
.get()
.unwrap()
.search_recordings(imp.work.borrow().as_ref().unwrap(), search)
.unwrap();
imp.recording_list.remove_all();
for recording in &recordings {
let mut label = recording.performers_string();
if let Some(year) = recording.year {
label.push_str(&format!(" ({year})"));
}
let row = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(&label)
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&label));
let recording = recording.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
obj.select(recording.clone());
});
imp.recording_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 recording"))
.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.recording_list.append(&create_row);
imp.recordings.replace(recordings);
}
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_work(&self, work: Work) {
self.imp().work_label.set_text(work.name.get());
self.imp().recording_search_entry.set_text("");
self.imp().recording_search_entry.grab_focus();
self.imp()
.recording_scrolled_window
.vadjustment()
.set_value(0.0);
self.imp()
.stack
.set_visible_child(&*self.imp().recording_view);
self.imp().work.replace(Some(work.clone()));
self.search_recordings("");
}
fn select(&self, recording: Recording) {
self.emit_by_name::<()>("selected", &[&recording]);
self.popdown();
}
fn create(&self) {
self.emit_by_name::<()>("create", &[]);
self.popdown();
}
}

240
src/editor/tracks_editor.rs Normal file
View file

@ -0,0 +1,240 @@
use super::tracks_editor_track_row::{PathType, TracksEditorTrackData};
use crate::{
db::models::Recording,
editor::{
recording_editor::MusicusRecordingEditor,
recording_selector_popover::RecordingSelectorPopover,
tracks_editor_track_row::TracksEditorTrackRow,
},
library::MusicusLibrary,
};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, clone, subclass::Signal, Properties},
};
use once_cell::sync::Lazy;
use std::{
cell::{OnceCell, RefCell},
path::PathBuf,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::TracksEditor)]
#[template(file = "data/ui/tracks_editor.blp")]
pub struct TracksEditor {
#[property(get, construct_only)]
pub navigation: OnceCell<adw::NavigationView>,
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub recording: RefCell<Option<Recording>>,
pub recordings_popover: OnceCell<RecordingSelectorPopover>,
pub track_rows: RefCell<Vec<TracksEditorTrackRow>>,
#[template_child]
pub recording_row: TemplateChild<adw::ActionRow>,
#[template_child]
pub select_recording_box: TemplateChild<gtk::Box>,
#[template_child]
pub track_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub save_row: TemplateChild<adw::ButtonRow>,
}
#[glib::object_subclass]
impl ObjectSubclass for TracksEditor {
const NAME: &'static str = "MusicusTracksEditor";
type Type = super::TracksEditor;
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 TracksEditor {
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 recordings_popover = RecordingSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().clone();
recordings_popover.connect_selected(move |_, recording| {
obj.set_recording(recording);
});
let obj = self.obj().clone();
recordings_popover.connect_create(move |_| {
let editor = MusicusRecordingEditor::new(
obj.imp().navigation.get().unwrap(),
&obj.library(),
None,
);
editor.connect_created(clone!(
#[weak]
obj,
move |_, recording| {
obj.set_recording(recording);
}
));
obj.imp().navigation.get().unwrap().push(&editor);
});
self.select_recording_box.append(&recordings_popover);
self.recordings_popover.set(recordings_popover).unwrap();
}
}
impl WidgetImpl for TracksEditor {}
impl NavigationPageImpl for TracksEditor {}
}
glib::wrapper! {
pub struct TracksEditor(ObjectSubclass<imp::TracksEditor>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl TracksEditor {
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_row.set_title(&gettext("Save changes"));
obj.set_recording(recording);
}
obj
}
#[template_callback]
fn select_recording(&self, _: &adw::ActionRow) {
self.imp().recordings_popover.get().unwrap().popup();
}
#[template_callback]
async fn add_files(&self, _: &adw::ActionRow) {
let dialog = gtk::FileDialog::builder()
.title(gettext("Select audio files"))
.modal(true)
.build();
let root = self.root();
let window = root
.as_ref()
.and_then(|r| r.downcast_ref::<gtk::Window>())
.unwrap();
let obj = self.clone();
match dialog.open_multiple_future(Some(window)).await {
Err(err) => {
if !err.matches(gtk::DialogError::Dismissed) {
log::error!("File selection failed: {err}");
}
}
Ok(files) => {
for file in &files {
obj.add_file(
file.unwrap()
.downcast::<gio::File>()
.unwrap()
.path()
.unwrap(),
);
}
}
}
}
fn set_recording(&self, recording: Recording) {
self.imp().recording_row.set_title(&format!(
"{}: {}",
recording.work.composers_string(),
recording.work.name.get(),
));
self.imp()
.recording_row
.set_subtitle(&recording.performers_string());
for track in self
.library()
.tracks_for_recording(&recording.recording_id)
.unwrap()
{
self.add_track_row(TracksEditorTrackData {
track_id: Some(track.track_id),
path: PathType::Library(track.path),
works: track.works,
});
}
self.imp().recording.replace(Some(recording));
}
fn add_file(&self, path: PathBuf) {
self.add_track_row(TracksEditorTrackData {
track_id: None,
path: PathType::System(path),
works: Vec::new(),
});
}
fn add_track_row(&self, track_data: TracksEditorTrackData) {
let track_row = TracksEditorTrackRow::new(&self.navigation(), &self.library(), track_data);
track_row.connect_remove(clone!(
#[weak(rename_to = this)]
self,
move |row| {
this.imp().track_list.remove(row);
this.imp().track_rows.borrow_mut().retain(|p| p != row);
}
));
self.imp()
.track_list
.insert(&track_row, self.imp().track_rows.borrow().len() as i32);
self.imp().track_rows.borrow_mut().push(track_row);
}
#[template_callback]
fn save(&self) {
// TODO
self.navigation().pop();
}
}

View file

@ -0,0 +1,149 @@
use crate::{db::models::Work, library::MusicusLibrary};
use adw::{prelude::*, subclass::prelude::*};
use formatx::formatx;
use gettextrs::gettext;
use gtk::glib::{self, clone, subclass::Signal, Properties};
use once_cell::sync::Lazy;
use std::{
cell::{OnceCell, RefCell},
path::PathBuf,
};
mod imp {
use super::*;
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
#[properties(wrapper_type = super::TracksEditorTrackRow)]
#[template(file = "data/ui/tracks_editor_track_row.blp")]
pub struct TracksEditorTrackRow {
#[property(get, construct_only)]
pub navigation: OnceCell<adw::NavigationView>,
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub track_data: RefCell<TracksEditorTrackData>,
#[template_child]
pub select_parts_box: TemplateChild<gtk::Box>,
}
#[glib::object_subclass]
impl ObjectSubclass for TracksEditorTrackRow {
const NAME: &'static str = "MusicusTracksEditorTrackRow";
type Type = super::TracksEditorTrackRow;
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 TracksEditorTrackRow {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
SIGNALS.as_ref()
}
}
impl WidgetImpl for TracksEditorTrackRow {}
impl ListBoxRowImpl for TracksEditorTrackRow {}
impl PreferencesRowImpl for TracksEditorTrackRow {}
impl ActionRowImpl for TracksEditorTrackRow {}
}
glib::wrapper! {
pub struct TracksEditorTrackRow(ObjectSubclass<imp::TracksEditorTrackRow>)
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow;
}
#[gtk::template_callbacks]
impl TracksEditorTrackRow {
pub fn new(
navigation: &adw::NavigationView,
library: &MusicusLibrary,
track_data: TracksEditorTrackData,
) -> Self {
let obj: Self = glib::Object::builder()
.property("navigation", navigation)
.property("library", library)
.build();
obj.set_subtitle(&match &track_data.path {
PathType::None => String::new(),
PathType::Library(path) => path.to_owned(),
PathType::System(path) => {
let format_string = gettext("Import from {}");
let file_name = path.file_name().unwrap().to_str().unwrap();
match formatx!(&format_string, file_name) {
Ok(title) => title,
Err(_) => {
log::error!("Error in translated format string: {format_string}");
file_name.to_owned()
}
}
}
});
obj.set_works(&track_data.works);
obj.imp().track_data.replace(track_data);
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 track_data(&self) -> TracksEditorTrackData {
self.imp().track_data.borrow().to_owned()
}
#[template_callback]
fn select_parts(&self) {
// self.imp().parts_popover.get().unwrap().popup();
}
#[template_callback]
fn remove(&self) {
self.emit_by_name::<()>("remove", &[]);
}
fn set_works(&self, works: &[Work]) {
self.set_title(
&works
.iter()
.map(|w| w.name.get())
.collect::<Vec<&str>>()
.join(", "),
);
}
}
#[derive(Clone, Default, Debug)]
pub struct TracksEditorTrackData {
pub track_id: Option<String>,
pub path: PathType,
pub works: Vec<Work>,
}
#[derive(Clone, Default, Debug)]
pub enum PathType {
#[default]
None,
Library(String),
System(PathBuf),
}

View file

@ -115,7 +115,7 @@ impl MusicusLibrary {
.collect::<Result<Vec<Ensemble>>>()?; .collect::<Result<Vec<Ensemble>>>()?;
let works: Vec<Work> = works::table let works: Vec<Work> = works::table
.inner_join(work_persons::table.inner_join(persons::table)) .left_join(work_persons::table.inner_join(persons::table))
.filter(works::name.like(&search).or(persons::name.like(&search))) .filter(works::name.like(&search).or(persons::name.like(&search)))
.limit(9) .limit(9)
.select(works::all_columns) .select(works::all_columns)
@ -225,7 +225,7 @@ impl MusicusLibrary {
let recordings = recordings::table let recordings = recordings::table
.inner_join( .inner_join(
works::table.inner_join(work_persons::table.inner_join(persons::table)), works::table.left_join(work_persons::table.inner_join(persons::table)),
) )
// .inner_join(recording_persons::table.inner_join(persons::table)) // .inner_join(recording_persons::table.inner_join(persons::table))
.inner_join(recording_ensembles::table) .inner_join(recording_ensembles::table)
@ -287,7 +287,7 @@ impl MusicusLibrary {
let recordings = recordings::table let recordings = recordings::table
.inner_join( .inner_join(
works::table.inner_join(work_persons::table.inner_join(persons::table)), works::table.left_join(work_persons::table.inner_join(persons::table)),
) )
.inner_join(recording_persons::table) .inner_join(recording_persons::table)
.filter( .filter(
@ -400,10 +400,10 @@ impl MusicusLibrary {
let connection = &mut *binding.as_mut().unwrap(); let connection = &mut *binding.as_mut().unwrap();
let mut query = recordings::table let mut query = recordings::table
.inner_join(works::table.inner_join(work_persons::table)) .inner_join(works::table.left_join(work_persons::table))
.inner_join(recording_persons::table) .left_join(recording_persons::table)
.inner_join(recording_ensembles::table) .left_join(recording_ensembles::table)
.inner_join(album_recordings::table) .left_join(album_recordings::table)
.into_boxed(); .into_boxed();
if let Some(composer_id) = program.composer_id() { if let Some(composer_id) = program.composer_id() {
@ -556,7 +556,7 @@ impl MusicusLibrary {
let connection = &mut *binding.as_mut().unwrap(); let connection = &mut *binding.as_mut().unwrap();
let works: Vec<Work> = works::table let works: Vec<Work> = works::table
.inner_join(work_persons::table) .left_join(work_persons::table)
.filter( .filter(
works::name works::name
.like(&search) .like(&search)
@ -573,6 +573,32 @@ impl MusicusLibrary {
Ok(works) Ok(works)
} }
pub fn search_recordings(&self, work: &Work, search: &str) -> Result<Vec<Recording>> {
let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let recordings = recordings::table
.left_join(recording_persons::table.inner_join(persons::table))
.left_join(recording_ensembles::table.inner_join(ensembles::table))
.filter(
recordings::work_id.eq(&work.work_id).and(
persons::name
.like(&search)
.or(ensembles::name.like(&search)),
),
)
.limit(9)
.select(recordings::all_columns)
.distinct()
.load::<tables::Recording>(connection)?
.into_iter()
.map(|r| Recording::from_table(r, connection))
.collect::<Result<Vec<Recording>>>()?;
Ok(recordings)
}
pub fn all_works(&self) -> Result<Vec<Work>> { pub fn all_works(&self) -> Result<Vec<Work>> {
let mut binding = self.imp().connection.borrow_mut(); let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap(); let connection = &mut *binding.as_mut().unwrap();
@ -1259,4 +1285,4 @@ impl LibraryResults {
&& self.recordings.is_empty() && self.recordings.is_empty()
&& self.albums.is_empty() && self.albums.is_empty()
} }
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
config, home_page::MusicusHomePage, library::MusicusLibrary, library_manager::LibraryManager, config, editor::tracks_editor::TracksEditor, home_page::MusicusHomePage,
player::MusicusPlayer, player_bar::PlayerBar, playlist_page::MusicusPlaylistPage, library::MusicusLibrary, library_manager::LibraryManager, player::MusicusPlayer,
welcome_page::MusicusWelcomePage, player_bar::PlayerBar, playlist_page::MusicusPlaylistPage, welcome_page::MusicusWelcomePage,
}; };
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
@ -15,8 +15,8 @@ mod imp {
#[derive(Debug, Default, gtk::CompositeTemplate)] #[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/window.blp")] #[template(file = "data/ui/window.blp")]
pub struct MusicusWindow { pub struct MusicusWindow {
pub library: RefCell<Option<MusicusLibrary>>,
pub player: MusicusPlayer, pub player: MusicusPlayer,
pub library_manager: RefCell<Option<LibraryManager>>,
#[template_child] #[template_child]
pub stack: TemplateChild<gtk::Stack>, pub stack: TemplateChild<gtk::Stack>,
@ -52,14 +52,29 @@ mod imp {
self.obj().add_css_class("devel"); self.obj().add_css_class("devel");
} }
let navigation_view = self.navigation_view.get().to_owned(); let obj = self.obj().to_owned();
let library_action = gio::ActionEntry::builder("library") let import_action = gio::ActionEntry::builder("import")
.activate(move |_: &super::MusicusWindow, _, _| { .activate(move |_, _, _| {
navigation_view.push_by_tag("library") if let Some(library) = &*obj.imp().library.borrow() {
let editor = TracksEditor::new(&obj.imp().navigation_view, library, None);
obj.imp().navigation_view.push(&editor);
}
}) })
.build(); .build();
self.obj().add_action_entries([library_action]); let obj = self.obj().to_owned();
let library_action = gio::ActionEntry::builder("library")
.activate(move |_, _, _| {
if let Some(library) = &*obj.imp().library.borrow() {
let library_manager =
LibraryManager::new(&obj.imp().navigation_view, library);
obj.imp().navigation_view.push(&library_manager);
}
})
.build();
self.obj()
.add_action_entries([import_action, library_action]);
let player_bar = PlayerBar::new(&self.player); let player_bar = PlayerBar::new(&self.player);
self.player_bar_revealer.set_child(Some(&player_bar)); self.player_bar_revealer.set_child(Some(&player_bar));
@ -174,16 +189,9 @@ impl MusicusWindow {
self.imp().player.set_library(&library); self.imp().player.set_library(&library);
let navigation = self.imp().navigation_view.get(); let navigation = self.imp().navigation_view.get();
if let Some(library_manager) = self.imp().library_manager.take() {
navigation.remove(&library_manager);
}
let library_manager = LibraryManager::new(&navigation, &library);
navigation navigation
.replace(&[MusicusHomePage::new(&navigation, &library, &self.imp().player).into()]); .replace(&[MusicusHomePage::new(&navigation, &library, &self.imp().player).into()]);
navigation.add(&library_manager);
self.imp().library_manager.replace(Some(library_manager)); self.imp().library.replace(Some(library));
} }
} }