mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
editor: Add album editor
This commit is contained in:
parent
c4162fd98a
commit
a16dc446d6
8 changed files with 638 additions and 116 deletions
78
data/ui/album_editor.blp
Normal file
78
data/ui/album_editor.blp
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $MusicusAlbumEditor: Adw.NavigationPage {
|
||||||
|
title: _("Album");
|
||||||
|
|
||||||
|
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.Label {
|
||||||
|
label: _("Recordings");
|
||||||
|
xalign: 0;
|
||||||
|
margin-top: 24;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"heading"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.ListBox recordings_list {
|
||||||
|
selection-mode: none;
|
||||||
|
margin-top: 12;
|
||||||
|
margin-bottom: 24;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"boxed-list"
|
||||||
|
]
|
||||||
|
|
||||||
|
Adw.ActionRow {
|
||||||
|
title: _("Add recording");
|
||||||
|
activatable: true;
|
||||||
|
activated => $select_recording() swapped;
|
||||||
|
|
||||||
|
[prefix]
|
||||||
|
Gtk.Box select_recording_box {
|
||||||
|
Gtk.Image {
|
||||||
|
icon-name: "list-add-symbolic";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.ListBox {
|
||||||
|
selection-mode: none;
|
||||||
|
margin-top: 24;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"boxed-list"
|
||||||
|
]
|
||||||
|
|
||||||
|
Adw.ButtonRow save_row {
|
||||||
|
title: _("Create album");
|
||||||
|
activated => $save() swapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
data/ui/library_manager_albums_page.blp
Normal file
42
data/ui/library_manager_albums_page.blp
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $MusicusLibraryManagerAlbumsPage: Adw.NavigationPage {
|
||||||
|
title: _("Albums");
|
||||||
|
|
||||||
|
Adw.ToolbarView {
|
||||||
|
[top]
|
||||||
|
Gtk.Box {
|
||||||
|
orientation: vertical;
|
||||||
|
|
||||||
|
Adw.HeaderBar {
|
||||||
|
[end]
|
||||||
|
Gtk.Button {
|
||||||
|
icon-name: "list-add-symbolic";
|
||||||
|
clicked => $create() swapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.Clamp {
|
||||||
|
Gtk.SearchEntry search_entry {
|
||||||
|
placeholder-text: _("Search albums…");
|
||||||
|
search-changed => $search_changed() swapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.ScrolledWindow {
|
||||||
|
Adw.Clamp {
|
||||||
|
Gtk.ListBox list {
|
||||||
|
selection-mode: none;
|
||||||
|
margin-top: 12;
|
||||||
|
valign: start;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"boxed-list"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ use gtk::glib::{self, Boxed};
|
||||||
use super::{schema::*, tables, TranslatedString};
|
use super::{schema::*, tables, TranslatedString};
|
||||||
|
|
||||||
// Re-exports for tables that don't need additional information.
|
// Re-exports for tables that don't need additional information.
|
||||||
pub use tables::{Album, Instrument, Person, Role};
|
pub use tables::{Instrument, Person, Role};
|
||||||
|
|
||||||
#[derive(Boxed, Clone, Debug)]
|
#[derive(Boxed, Clone, Debug)]
|
||||||
#[boxed_type(name = "MusicusWork")]
|
#[boxed_type(name = "MusicusWork")]
|
||||||
|
|
@ -68,6 +68,14 @@ pub struct Track {
|
||||||
pub works: Vec<Work>,
|
pub works: Vec<Work>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Boxed, Clone, Debug)]
|
||||||
|
#[boxed_type(name = "MusicusAlbum")]
|
||||||
|
pub struct Album {
|
||||||
|
pub album_id: String,
|
||||||
|
pub name: TranslatedString,
|
||||||
|
pub recordings: Vec<Recording>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Eq for Person {}
|
impl Eq for Person {}
|
||||||
impl PartialEq for Person {
|
impl PartialEq for Person {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
|
@ -268,6 +276,13 @@ impl Recording {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Eq for Recording {}
|
||||||
|
impl PartialEq for Recording {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.recording_id == other.recording_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for Recording {
|
impl Display for Recording {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}; {}", self.work, self.performers_string())
|
write!(f, "{}; {}", self.work, self.performers_string())
|
||||||
|
|
@ -360,9 +375,35 @@ impl Track {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Album {
|
||||||
|
pub fn from_table(data: tables::Album, connection: &mut SqliteConnection) -> Result<Self> {
|
||||||
|
let recordings: Vec<Recording> = recordings::table
|
||||||
|
.inner_join(album_recordings::table)
|
||||||
|
.order(album_recordings::sequence_number)
|
||||||
|
.filter(album_recordings::album_id.eq(&data.album_id))
|
||||||
|
.select(tables::Recording::as_select())
|
||||||
|
.load(connection)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| Recording::from_table(r, connection))
|
||||||
|
.collect::<Result<Vec<Recording>>>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
album_id: data.album_id,
|
||||||
|
name: data.name,
|
||||||
|
recordings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Eq for Album {}
|
impl Eq for Album {}
|
||||||
impl PartialEq for Album {
|
impl PartialEq for Album {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.album_id == other.album_id
|
self.album_id == other.album_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Album {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
208
src/editor/album_editor.rs
Normal file
208
src/editor/album_editor.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
use crate::{
|
||||||
|
db::models::{Album, Recording},
|
||||||
|
editor::{
|
||||||
|
recording_editor::MusicusRecordingEditor,
|
||||||
|
recording_selector_popover::RecordingSelectorPopover,
|
||||||
|
translation_editor::MusicusTranslationEditor,
|
||||||
|
},
|
||||||
|
library::MusicusLibrary,
|
||||||
|
};
|
||||||
|
|
||||||
|
use adw::{prelude::*, subclass::prelude::*};
|
||||||
|
use gettextrs::gettext;
|
||||||
|
use gtk::glib::{
|
||||||
|
clone, Properties,
|
||||||
|
{self, subclass::Signal},
|
||||||
|
};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use std::cell::{OnceCell, RefCell};
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
|
||||||
|
#[properties(wrapper_type = super::AlbumEditor)]
|
||||||
|
#[template(file = "data/ui/album_editor.blp")]
|
||||||
|
pub struct AlbumEditor {
|
||||||
|
#[property(get, construct_only)]
|
||||||
|
pub navigation: OnceCell<adw::NavigationView>,
|
||||||
|
#[property(get, construct_only)]
|
||||||
|
pub library: OnceCell<MusicusLibrary>,
|
||||||
|
|
||||||
|
pub album_id: OnceCell<String>,
|
||||||
|
pub recordings: RefCell<Vec<Recording>>,
|
||||||
|
|
||||||
|
pub recordings_popover: OnceCell<RecordingSelectorPopover>,
|
||||||
|
|
||||||
|
#[template_child]
|
||||||
|
pub name_editor: TemplateChild<MusicusTranslationEditor>,
|
||||||
|
#[template_child]
|
||||||
|
pub recordings_list: TemplateChild<gtk::ListBox>,
|
||||||
|
#[template_child]
|
||||||
|
pub select_recording_box: TemplateChild<gtk::Box>,
|
||||||
|
#[template_child]
|
||||||
|
pub save_row: TemplateChild<adw::ButtonRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for AlbumEditor {
|
||||||
|
const NAME: &'static str = "MusicusAlbumEditor";
|
||||||
|
type Type = super::AlbumEditor;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::derived_properties]
|
||||||
|
impl ObjectImpl for AlbumEditor {
|
||||||
|
fn signals() -> &'static [Signal] {
|
||||||
|
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||||
|
vec![Signal::builder("created")
|
||||||
|
.param_types([Album::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.add_recording(recording);
|
||||||
|
});
|
||||||
|
|
||||||
|
let obj = self.obj().clone();
|
||||||
|
recordings_popover.connect_create(move |_| {
|
||||||
|
let editor = MusicusRecordingEditor::new(&obj.navigation(), &obj.library(), None);
|
||||||
|
|
||||||
|
editor.connect_created(clone!(
|
||||||
|
#[weak]
|
||||||
|
obj,
|
||||||
|
move |_, recording| {
|
||||||
|
obj.add_recording(recording);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
obj.navigation().push(&editor);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.select_recording_box.append(&recordings_popover);
|
||||||
|
self.recordings_popover.set(recordings_popover).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetImpl for AlbumEditor {}
|
||||||
|
impl NavigationPageImpl for AlbumEditor {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct AlbumEditor(ObjectSubclass<imp::AlbumEditor>)
|
||||||
|
@extends gtk::Widget, adw::NavigationPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gtk::template_callbacks]
|
||||||
|
impl AlbumEditor {
|
||||||
|
pub fn new(
|
||||||
|
navigation: &adw::NavigationView,
|
||||||
|
library: &MusicusLibrary,
|
||||||
|
album: Option<&Album>,
|
||||||
|
) -> Self {
|
||||||
|
let obj: Self = glib::Object::builder()
|
||||||
|
.property("navigation", navigation)
|
||||||
|
.property("library", library)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if let Some(album) = album {
|
||||||
|
obj.imp().save_row.set_title(&gettext("Save changes"));
|
||||||
|
obj.imp().album_id.set(album.album_id.clone()).unwrap();
|
||||||
|
obj.imp().name_editor.set_translation(&album.name);
|
||||||
|
|
||||||
|
for recording in &album.recordings {
|
||||||
|
obj.add_recording(recording.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect_created<F: Fn(&Self, Album) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||||
|
self.connect_local("created", true, move |values| {
|
||||||
|
let obj = values[0].get::<Self>().unwrap();
|
||||||
|
let album = values[1].get::<Album>().unwrap();
|
||||||
|
f(&obj, album);
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn select_recording(&self) {
|
||||||
|
self.imp().recordings_popover.get().unwrap().popup();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_recording(&self, recording: Recording) {
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title(recording.work.to_string())
|
||||||
|
.subtitle(recording.performers_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]
|
||||||
|
recording,
|
||||||
|
move |_| {
|
||||||
|
this.imp().recordings_list.remove(&row);
|
||||||
|
this.imp()
|
||||||
|
.recordings
|
||||||
|
.borrow_mut()
|
||||||
|
.retain(|r| *r != recording);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
row.add_suffix(&remove_button);
|
||||||
|
|
||||||
|
self.imp()
|
||||||
|
.recordings_list
|
||||||
|
.insert(&row, self.imp().recordings.borrow().len() as i32);
|
||||||
|
|
||||||
|
self.imp().recordings.borrow_mut().push(recording);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn save(&self) {
|
||||||
|
let library = self.imp().library.get().unwrap();
|
||||||
|
|
||||||
|
let name = self.imp().name_editor.translation();
|
||||||
|
let recordings = self.imp().recordings.borrow().clone();
|
||||||
|
|
||||||
|
if let Some(album_id) = self.imp().album_id.get() {
|
||||||
|
library.update_album(album_id, name, recordings).unwrap();
|
||||||
|
} else {
|
||||||
|
let album = library.create_album(name, recordings).unwrap();
|
||||||
|
self.emit_by_name::<()>("created", &[&album]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.imp().navigation.get().unwrap().pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod activatable_row;
|
pub mod activatable_row;
|
||||||
|
pub mod album_editor;
|
||||||
pub mod ensemble_editor;
|
pub mod ensemble_editor;
|
||||||
pub mod ensemble_selector_popover;
|
pub mod ensemble_selector_popover;
|
||||||
pub mod instrument_editor;
|
pub mod instrument_editor;
|
||||||
|
|
|
||||||
109
src/library.rs
109
src/library.rs
|
|
@ -131,10 +131,13 @@ impl MusicusLibrary {
|
||||||
.map(|w| Work::from_table(w, connection))
|
.map(|w| Work::from_table(w, connection))
|
||||||
.collect::<Result<Vec<Work>>>()?;
|
.collect::<Result<Vec<Work>>>()?;
|
||||||
|
|
||||||
let albums: Vec<Album> = albums::table
|
let albums = albums::table
|
||||||
.filter(albums::name.like(&search))
|
.filter(albums::name.like(&search))
|
||||||
.limit(9)
|
.limit(9)
|
||||||
.load(connection)?;
|
.load::<tables::Album>(connection)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| Album::from_table(a, connection))
|
||||||
|
.collect::<Result<Vec<Album>>>()?;
|
||||||
|
|
||||||
LibraryResults {
|
LibraryResults {
|
||||||
composers,
|
composers,
|
||||||
|
|
@ -259,7 +262,10 @@ impl MusicusLibrary {
|
||||||
)
|
)
|
||||||
.select(albums::all_columns)
|
.select(albums::all_columns)
|
||||||
.distinct()
|
.distinct()
|
||||||
.load(connection)?;
|
.load::<tables::Album>(connection)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| Album::from_table(a, connection))
|
||||||
|
.collect::<Result<Vec<Album>>>()?;
|
||||||
|
|
||||||
LibraryResults {
|
LibraryResults {
|
||||||
composers,
|
composers,
|
||||||
|
|
@ -320,7 +326,10 @@ impl MusicusLibrary {
|
||||||
)
|
)
|
||||||
.select(albums::all_columns)
|
.select(albums::all_columns)
|
||||||
.distinct()
|
.distinct()
|
||||||
.load(connection)?;
|
.load::<tables::Album>(connection)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| Album::from_table(a, connection))
|
||||||
|
.collect::<Result<Vec<Album>>>()?;
|
||||||
|
|
||||||
LibraryResults {
|
LibraryResults {
|
||||||
composers,
|
composers,
|
||||||
|
|
@ -691,7 +700,11 @@ impl MusicusLibrary {
|
||||||
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();
|
||||||
|
|
||||||
let albums = albums::table.load::<Album>(connection)?;
|
let albums = albums::table
|
||||||
|
.load::<tables::Album>(connection)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| Album::from_table(a, connection))
|
||||||
|
.collect::<Result<Vec<Album>>>()?;
|
||||||
|
|
||||||
Ok(albums)
|
Ok(albums)
|
||||||
}
|
}
|
||||||
|
|
@ -1234,8 +1247,92 @@ impl MusicusLibrary {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_album(
|
||||||
|
&self,
|
||||||
|
name: TranslatedString,
|
||||||
|
recordings: Vec<Recording>,
|
||||||
|
) -> Result<Album> {
|
||||||
|
let mut binding = self.imp().connection.borrow_mut();
|
||||||
|
let connection = &mut *binding.as_mut().unwrap();
|
||||||
|
|
||||||
|
let album_id = db::generate_id();
|
||||||
|
let now = Local::now().naive_local();
|
||||||
|
|
||||||
|
let album_data = tables::Album {
|
||||||
|
album_id: album_id.clone(),
|
||||||
|
name,
|
||||||
|
created_at: now,
|
||||||
|
edited_at: now,
|
||||||
|
last_used_at: now,
|
||||||
|
last_played_at: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(albums::table)
|
||||||
|
.values(&album_data)
|
||||||
|
.execute(connection)?;
|
||||||
|
|
||||||
|
for (index, recording) in recordings.into_iter().enumerate() {
|
||||||
|
let album_recording_data = tables::AlbumRecording {
|
||||||
|
album_id: album_id.clone(),
|
||||||
|
recording_id: recording.recording_id,
|
||||||
|
sequence_number: index as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(album_recordings::table)
|
||||||
|
.values(&album_recording_data)
|
||||||
|
.execute(connection)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let album = Album::from_table(album_data, connection)?;
|
||||||
|
|
||||||
|
self.changed();
|
||||||
|
|
||||||
|
Ok(album)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_album(
|
||||||
|
&self,
|
||||||
|
album_id: &str,
|
||||||
|
name: TranslatedString,
|
||||||
|
recordings: Vec<Recording>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut binding = self.imp().connection.borrow_mut();
|
||||||
|
let connection = &mut *binding.as_mut().unwrap();
|
||||||
|
|
||||||
|
let now = Local::now().naive_local();
|
||||||
|
|
||||||
|
diesel::update(albums::table)
|
||||||
|
.filter(albums::album_id.eq(album_id))
|
||||||
|
.set((
|
||||||
|
albums::name.eq(name),
|
||||||
|
albums::edited_at.eq(now),
|
||||||
|
albums::last_used_at.eq(now),
|
||||||
|
))
|
||||||
|
.execute(connection)?;
|
||||||
|
|
||||||
|
diesel::delete(album_recordings::table)
|
||||||
|
.filter(album_recordings::album_id.eq(album_id))
|
||||||
|
.execute(connection)?;
|
||||||
|
|
||||||
|
for (index, recording) in recordings.into_iter().enumerate() {
|
||||||
|
let album_recording_data = tables::AlbumRecording {
|
||||||
|
album_id: album_id.to_owned(),
|
||||||
|
recording_id: recording.recording_id,
|
||||||
|
sequence_number: index as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(album_recordings::table)
|
||||||
|
.values(&album_recording_data)
|
||||||
|
.execute(connection)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.changed();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Import a track into the music library.
|
/// Import a track into the music library.
|
||||||
// TODO: Support mediums, think about albums.
|
// TODO: Support mediums.
|
||||||
pub fn import_track(
|
pub fn import_track(
|
||||||
&self,
|
&self,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
|
|
|
||||||
145
src/library_manager/albums_page.rs
Normal file
145
src/library_manager/albums_page.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
use crate::{db::models::Album, editor::album_editor::AlbumEditor, library::MusicusLibrary};
|
||||||
|
|
||||||
|
use adw::{prelude::*, subclass::prelude::*};
|
||||||
|
use gettextrs::gettext;
|
||||||
|
use gtk::glib::{self, clone};
|
||||||
|
|
||||||
|
use std::cell::{OnceCell, RefCell};
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||||
|
#[template(file = "data/ui/library_manager_albums_page.blp")]
|
||||||
|
pub struct AlbumsPage {
|
||||||
|
pub navigation: OnceCell<adw::NavigationView>,
|
||||||
|
pub library: OnceCell<MusicusLibrary>,
|
||||||
|
pub albums: RefCell<Vec<Album>>,
|
||||||
|
pub albums_filtered: RefCell<Vec<Album>>,
|
||||||
|
|
||||||
|
#[template_child]
|
||||||
|
pub search_entry: TemplateChild<gtk::SearchEntry>,
|
||||||
|
#[template_child]
|
||||||
|
pub list: TemplateChild<gtk::ListBox>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for AlbumsPage {
|
||||||
|
const NAME: &'static str = "MusicusLibraryManagerAlbumsPage";
|
||||||
|
type Type = super::AlbumsPage;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for AlbumsPage {}
|
||||||
|
impl WidgetImpl for AlbumsPage {}
|
||||||
|
|
||||||
|
impl NavigationPageImpl for AlbumsPage {
|
||||||
|
fn showing(&self) {
|
||||||
|
self.parent_showing();
|
||||||
|
self.obj().update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct AlbumsPage(ObjectSubclass<imp::AlbumsPage>)
|
||||||
|
@extends gtk::Widget, adw::NavigationPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gtk::template_callbacks]
|
||||||
|
impl AlbumsPage {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&self) {
|
||||||
|
let albums = self.imp().library.get().unwrap().all_albums().unwrap();
|
||||||
|
self.imp().albums.replace(albums);
|
||||||
|
self.search_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn search_changed(&self) {
|
||||||
|
let albums_filtered = self
|
||||||
|
.imp()
|
||||||
|
.albums
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.filter(|a| {
|
||||||
|
a.name
|
||||||
|
.get()
|
||||||
|
.contains(&self.imp().search_entry.text().to_string())
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<Album>>();
|
||||||
|
|
||||||
|
self.imp().list.remove_all();
|
||||||
|
|
||||||
|
for album in albums_filtered {
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title(album.name.get())
|
||||||
|
.activatable(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
row.connect_activated(clone!(
|
||||||
|
#[weak(rename_to = obj)]
|
||||||
|
self,
|
||||||
|
#[strong]
|
||||||
|
album,
|
||||||
|
move |_| {
|
||||||
|
obj.imp().navigation.get().unwrap().push(&AlbumEditor::new(
|
||||||
|
&obj.imp().navigation.get().unwrap(),
|
||||||
|
&obj.imp().library.get().unwrap(),
|
||||||
|
Some(&album),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let delete_button = gtk::Button::builder()
|
||||||
|
.icon_name("user-trash-symbolic")
|
||||||
|
.tooltip_text(gettext("Delete album"))
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.css_classes(["flat"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// delete_button.connect_clicked(clone!(
|
||||||
|
// #[weak(rename_to = obj)]
|
||||||
|
// self,
|
||||||
|
// #[strong]
|
||||||
|
// album,
|
||||||
|
// move |_| {
|
||||||
|
// obj.imp().library.delete_album(&album.album_id).unwrap();
|
||||||
|
// }
|
||||||
|
// ));
|
||||||
|
|
||||||
|
row.add_suffix(&delete_button);
|
||||||
|
|
||||||
|
self.imp().list.append(&row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn create(&self) {
|
||||||
|
self.imp().navigation.get().unwrap().push(&AlbumEditor::new(
|
||||||
|
&self.imp().navigation.get().unwrap(),
|
||||||
|
&self.imp().library.get().unwrap(),
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
pub mod albums_page;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
models::{Album, Ensemble, Instrument, Person, Recording, Role, Track, Work},
|
models::{Album, Ensemble, Instrument, Person, Recording, Role, Track, Work},
|
||||||
|
|
@ -8,6 +10,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use adw::{prelude::*, subclass::prelude::*};
|
use adw::{prelude::*, subclass::prelude::*};
|
||||||
|
use albums_page::AlbumsPage;
|
||||||
use gettextrs::gettext;
|
use gettextrs::gettext;
|
||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
|
|
||||||
|
|
@ -103,7 +106,7 @@ impl LibraryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
async fn open_library(&self, _: &adw::ActionRow) {
|
async fn open_library(&self) {
|
||||||
let dialog = gtk::FileDialog::builder()
|
let dialog = gtk::FileDialog::builder()
|
||||||
.title(gettext("Select music library folder"))
|
.title(gettext("Select music library folder"))
|
||||||
.modal(true)
|
.modal(true)
|
||||||
|
|
@ -127,37 +130,41 @@ impl LibraryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn import_archive(&self, _: &adw::ButtonRow) {}
|
fn import_archive(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn export_archive(&self, _: &adw::ButtonRow) {}
|
fn export_archive(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_persons(&self, _: &adw::ActionRow) {}
|
fn show_persons(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_roles(&self, _: &adw::ActionRow) {}
|
fn show_roles(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_instruments(&self, _: &adw::ActionRow) {}
|
fn show_instruments(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_works(&self, _: &adw::ActionRow) {}
|
fn show_works(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_ensembles(&self, _: &adw::ActionRow) {}
|
fn show_ensembles(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_recordings(&self, _: &adw::ActionRow) {}
|
fn show_recordings(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_tracks(&self, _: &adw::ActionRow) {}
|
fn show_tracks(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_mediums(&self, _: &adw::ActionRow) {}
|
fn show_mediums(&self) {}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn show_albums(&self, _: &adw::ActionRow) {}
|
fn show_albums(&self) {
|
||||||
|
let navigation = self.imp().navigation.get().unwrap();
|
||||||
|
let library = self.imp().library.get().unwrap();
|
||||||
|
navigation.push(&AlbumsPage::new(navigation, library));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Make this async.
|
// TODO: Make this async.
|
||||||
fn update(&self) {
|
fn update(&self) {
|
||||||
|
|
@ -217,101 +224,4 @@ impl LibraryManager {
|
||||||
.set_label(&albums.len().to_string());
|
.set_label(&albums.len().to_string());
|
||||||
self.imp().albums.replace(albums);
|
self.imp().albums.replace(albums);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[template_callback]
|
|
||||||
// fn add_person(&self) {
|
|
||||||
// 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) {
|
|
||||||
// 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) {
|
|
||||||
// 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) {
|
|
||||||
// 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) {
|
|
||||||
// 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) {
|
|
||||||
// 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) {
|
|
||||||
// todo!("Medium import");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[template_callback]
|
|
||||||
// fn add_album(&self) {
|
|
||||||
// todo!("Album editor");
|
|
||||||
// // self.imp()
|
|
||||||
// // .navigation
|
|
||||||
// // .get()
|
|
||||||
// // .unwrap()
|
|
||||||
// // .push(&MusicusAlbumEditor::new(
|
|
||||||
// // &self.imp().navigation.get().unwrap(),
|
|
||||||
// // &self.imp().library.get().unwrap(),
|
|
||||||
// // None,
|
|
||||||
// // ));
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue