Implement deletion

This commit is contained in:
Elias Projahn 2025-03-09 14:27:50 +01:00
parent b25d7fe8ee
commit 751dcde351
8 changed files with 340 additions and 40 deletions

39
data/ui/error_dialog.blp Normal file
View file

@ -0,0 +1,39 @@
using Gtk 4.0;
using Adw 1;
template $MusicusErrorDialog: Adw.Dialog {
content-width: 600;
content-height: 400;
Adw.ToastOverlay toast_overlay {
Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: _("Error");
};
[end]
Gtk.Button {
icon-name: "edit-copy-symbolic";
tooltip-text: _("Copy details to clipboard");
clicked => $copy() swapped;
styles [
"flat",
]
}
}
Gtk.ScrolledWindow {
Gtk.Label error_label {
xalign: 0.0;
margin-start: 12;
margin-end: 12;
margin-top: 12;
margin-bottom: 12;
}
}
}
}
}

View file

@ -1,27 +1,30 @@
using Gtk 4.0;
using Adw 1;
template $MusicusWindow : Adw.ApplicationWindow {
template $MusicusWindow: Adw.ApplicationWindow {
title: _("Musicus");
Adw.ToolbarView {
Gtk.Stack stack {
transition-type: over_up_down;
Adw.ToastOverlay toast_overlay {
Adw.ToolbarView {
Gtk.Stack stack {
transition-type: over_up_down;
Gtk.StackPage {
name: "navigation";
child: Adw.NavigationView navigation_view {
$MusicusWelcomePage {
folder-selected => $set_library_folder() swapped;
}
};
Gtk.StackPage {
name: "navigation";
child: Adw.NavigationView navigation_view {
$MusicusWelcomePage {
folder-selected => $set_library_folder() swapped;
}
};
}
}
[bottom]
Gtk.Revealer player_bar_revealer {
reveal-child: true;
transition-type: slide_up;
}
}
[bottom]
Gtk.Revealer player_bar_revealer {
reveal-child: true;
transition-type: slide_up;
}
}
}
}

View file

@ -1,15 +1,16 @@
use std::cell::OnceCell;
use adw::subclass::prelude::*;
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, Properties},
glib::{self, clone, Properties},
prelude::*,
};
use crate::{
db::models::*, editor::album::AlbumEditor, library::Library, player::Player,
playlist_item::PlaylistItem, recording_tile::RecordingTile,
playlist_item::PlaylistItem, recording_tile::RecordingTile, util::error_dialog::ErrorDialog,
};
mod imp {
@ -19,6 +20,9 @@ mod imp {
#[properties(wrapper_type = super::AlbumPage)]
#[template(file = "data/ui/album_page.blp")]
pub struct AlbumPage {
#[property(get, construct_only)]
pub toast_overlay: OnceCell<adw::ToastOverlay>,
#[property(get, construct_only)]
pub navigation: OnceCell<adw::NavigationView>,
@ -90,10 +94,28 @@ mod imp {
})
.build();
// let obj = self.obj().to_owned();
let obj = self.obj().to_owned();
let delete_action = gio::ActionEntry::builder("delete")
.activate(move |_, _, _| {
log::error!("Delete not implemented");
if let Err(err) = obj
.library()
.delete_album(&obj.imp().album.get().unwrap().album_id)
{
let toast = adw::Toast::builder()
.title(&gettext("Failed to delete album"))
.button_label("Details")
.build();
toast.connect_button_clicked(clone!(
#[weak]
obj,
move |_| {
ErrorDialog::present(&err, &obj);
}
));
obj.toast_overlay().add_toast(toast);
}
})
.build();
@ -115,12 +137,14 @@ glib::wrapper! {
#[gtk::template_callbacks]
impl AlbumPage {
pub fn new(
toast_overlay: &adw::ToastOverlay,
navigation: &adw::NavigationView,
library: &Library,
player: &Player,
album: Album,
) -> Self {
let obj: Self = glib::Object::builder()
.property("toast-overlay", toast_overlay)
.property("navigation", navigation)
.property("library", library)
.property("player", player)

View file

@ -879,6 +879,18 @@ impl Library {
Ok(())
}
pub fn delete_person(&self, person_id: &str) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
diesel::delete(persons::table)
.filter(persons::person_id.eq(person_id))
.execute(connection)?;
self.changed();
Ok(())
}
pub fn create_instrument(&self, name: TranslatedString) -> Result<Instrument> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
@ -921,6 +933,18 @@ impl Library {
Ok(())
}
pub fn delete_instrument(&self, instrument_id: &str) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
diesel::delete(instruments::table)
.filter(instruments::instrument_id.eq(instrument_id))
.execute(connection)?;
self.changed();
Ok(())
}
pub fn create_role(&self, name: TranslatedString) -> Result<Role> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
@ -962,6 +986,18 @@ impl Library {
Ok(())
}
pub fn delete_role(&self, role_id: &str) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
diesel::delete(roles::table)
.filter(roles::role_id.eq(role_id))
.execute(connection)?;
self.changed();
Ok(())
}
pub fn create_work(
&self,
name: TranslatedString,
@ -1175,6 +1211,18 @@ impl Library {
Ok(())
}
pub fn delete_work(&self, work_id: &str) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
diesel::delete(works::table)
.filter(works::work_id.eq(work_id))
.execute(connection)?;
self.changed();
Ok(())
}
pub fn create_ensemble(&self, name: TranslatedString) -> Result<Ensemble> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
@ -1223,6 +1271,18 @@ impl Library {
Ok(())
}
pub fn delete_ensemble(&self, ensemble_id: &str) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
diesel::delete(ensembles::table)
.filter(ensembles::ensemble_id.eq(ensemble_id))
.execute(connection)?;
self.changed();
Ok(())
}
pub fn create_recording(
&self,
work: Work,
@ -1345,6 +1405,18 @@ impl Library {
Ok(())
}
pub fn delete_recording(&self, recording_id: &str) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
diesel::delete(recordings::table)
.filter(recordings::recording_id.eq(recording_id))
.execute(connection)?;
self.changed();
Ok(())
}
pub fn create_album(
&self,
name: TranslatedString,
@ -1427,6 +1499,18 @@ impl Library {
Ok(())
}
pub fn delete_album(&self, album_id: &str) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
diesel::delete(albums::table)
.filter(albums::album_id.eq(album_id))
.execute(connection)?;
self.changed();
Ok(())
}
/// Import a track into the music library.
// TODO: Support mediums.
pub fn import_track(

View file

@ -5,7 +5,7 @@ use formatx::formatx;
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, Properties},
glib::{self, clone, Properties},
prelude::*,
};
@ -25,6 +25,7 @@ use crate::{
recording_tile::RecordingTile,
search_tag::Tag,
tag_tile::TagTile,
util::error_dialog::ErrorDialog,
};
mod imp {
@ -34,6 +35,9 @@ mod imp {
#[properties(wrapper_type = super::SearchPage)]
#[template(file = "data/ui/search_page.blp")]
pub struct SearchPage {
#[property(get, construct_only)]
pub toast_overlay: OnceCell<adw::ToastOverlay>,
#[property(get, construct_only)]
pub navigation: OnceCell<adw::NavigationView>,
@ -162,12 +166,14 @@ glib::wrapper! {
#[gtk::template_callbacks]
impl SearchPage {
pub fn new(
toast_overlay: &adw::ToastOverlay,
navigation: &adw::NavigationView,
library: &Library,
player: &Player,
query: LibraryQuery,
) -> Self {
let obj: Self = glib::Object::builder()
.property("toast-overlay", toast_overlay)
.property("navigation", navigation)
.property("library", library)
.property("player", player)
@ -230,24 +236,82 @@ impl SearchPage {
}
fn delete(&self) {
log::warn!("Deletion not implemented");
if let Some(highlight) = &*self.imp().highlight.borrow() {
match highlight {
Tag::Composer(person) | Tag::Performer(person) => {
if let Err(err) = self.library().delete_person(&person.person_id) {
let toast = adw::Toast::builder()
.title(&gettext("Failed to delete person"))
.button_label("Details")
.build();
// if let Some(highlight) = &*self.imp().highlight.borrow() {
// match highlight {
// Tag::Composer(person) | Tag::Performer(person) => {
// // TODO
// }
// Tag::Ensemble(ensemble) => {
// // TODO
// }
// Tag::Instrument(instrument) => {
// // TODO
// }
// Tag::Work(work) => {
// // TODO
// }
// }
// }
toast.connect_button_clicked(clone!(
#[weak(rename_to = obj)]
self,
move |_| {
ErrorDialog::present(&err, &obj);
}
));
self.toast_overlay().add_toast(toast);
}
}
Tag::Ensemble(ensemble) => {
if let Err(err) = self.library().delete_ensemble(&ensemble.ensemble_id) {
let toast = adw::Toast::builder()
.title(&gettext("Failed to delete ensemble"))
.button_label("Details")
.build();
toast.connect_button_clicked(clone!(
#[weak(rename_to = obj)]
self,
move |_| {
ErrorDialog::present(&err, &obj);
}
));
self.toast_overlay().add_toast(toast);
}
}
Tag::Instrument(instrument) => {
if let Err(err) = self.library().delete_instrument(&instrument.instrument_id) {
let toast = adw::Toast::builder()
.title(&gettext("Failed to delete instrument"))
.button_label("Details")
.build();
toast.connect_button_clicked(clone!(
#[weak(rename_to = obj)]
self,
move |_| {
ErrorDialog::present(&err, &obj);
}
));
self.toast_overlay().add_toast(toast);
}
}
Tag::Work(work) => {
if let Err(err) = self.library().delete_work(&work.work_id) {
let toast = adw::Toast::builder()
.title(&gettext("Failed to delete work"))
.button_label("Details")
.build();
toast.connect_button_clicked(clone!(
#[weak(rename_to = obj)]
self,
move |_| {
ErrorDialog::present(&err, &obj);
}
));
self.toast_overlay().add_toast(toast);
}
}
}
}
}
#[template_callback]
@ -297,6 +361,7 @@ impl SearchPage {
if query_changed {
self.navigation().push(&SearchPage::new(
&self.toast_overlay(),
&self.navigation(),
&self.library(),
&self.player(),
@ -325,6 +390,7 @@ impl SearchPage {
}
self.navigation().push(&SearchPage::new(
&self.toast_overlay(),
&self.navigation(),
&self.library(),
&self.player(),
@ -347,6 +413,7 @@ impl SearchPage {
fn show_album(&self, album: &Album) {
self.navigation().push(&AlbumPage::new(
&self.toast_overlay(),
&self.navigation(),
&self.library(),
&self.player(),

View file

@ -1,5 +1,6 @@
pub mod activatable_row;
pub mod drag_widget;
pub mod error_dialog;
use gtk::glib;
use lazy_static::lazy_static;

79
src/util/error_dialog.rs Normal file
View file

@ -0,0 +1,79 @@
use std::cell::OnceCell;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gdk,
glib::{self, Properties},
};
mod imp {
use super::*;
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
#[properties(wrapper_type = super::ErrorDialog)]
#[template(file = "data/ui/error_dialog.blp")]
pub struct ErrorDialog {
#[property(get, construct_only)]
pub error_text: OnceCell<String>,
#[template_child]
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
#[template_child]
pub error_label: TemplateChild<gtk::Label>,
}
#[glib::object_subclass]
impl ObjectSubclass for ErrorDialog {
const NAME: &'static str = "MusicusErrorDialog";
type Type = super::ErrorDialog;
type ParentType = adw::Dialog;
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 ErrorDialog {
fn constructed(&self) {
self.parent_constructed();
self.error_label.set_label(&self.obj().error_text());
}
}
impl WidgetImpl for ErrorDialog {}
impl AdwDialogImpl for ErrorDialog {}
}
glib::wrapper! {
pub struct ErrorDialog(ObjectSubclass<imp::ErrorDialog>)
@extends gtk::Widget, adw::Dialog;
}
#[gtk::template_callbacks]
impl ErrorDialog {
pub fn present(err: &anyhow::Error, parent: &impl IsA<gtk::Widget>) {
let obj: Self = glib::Object::builder()
.property("error-text", &format!("{err:?}"))
.build();
obj.present(Some(parent));
}
#[template_callback]
fn copy(&self) {
if let Some(display) = gdk::Display::default() {
display.clipboard().set_text(&self.error_text());
self.imp()
.toast_overlay
.add_toast(adw::Toast::new(&gettext("Copied to clipboard")));
}
}
}

View file

@ -29,6 +29,8 @@ mod imp {
pub player: Player,
pub process_manager: ProcessManager,
#[template_child]
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
@ -242,6 +244,7 @@ impl Window {
fn reset_view(&self) {
let navigation = self.imp().navigation_view.get();
navigation.replace(&[SearchPage::new(
&self.imp().toast_overlay,
&navigation,
self.imp().library.borrow().as_ref().unwrap(),
&self.imp().player,