diff --git a/data/res/style.css b/data/res/style.css index ad68c52..8f363d2 100644 --- a/data/res/style.css +++ b/data/res/style.css @@ -20,4 +20,18 @@ .tile .subtitle { font-size: smaller; +} + +.tile .work { + margin-top: 3px; +} + +.tile .composer { + font-size: smaller; +} + +.tile .performances { + margin-top: 3px; + margin-bottom: 3px; + font-size: smaller; } \ No newline at end of file diff --git a/data/ui/recording_tile.blp b/data/ui/recording_tile.blp new file mode 100644 index 0000000..84ce61b --- /dev/null +++ b/data/ui/recording_tile.blp @@ -0,0 +1,39 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusRecordingTile : Gtk.FlowBoxChild { + styles ["card", "activatable", "tile"] + + Gtk.Box { + spacing: 12; + + Gtk.Image { + icon-name: "media-playback-start-symbolic"; + valign: start; + margin-top: 12; + } + + Gtk.Box { + orientation: vertical; + hexpand: true; + + Gtk.Label work_label { + styles ["work"] + halign: start; + wrap: true; + } + + Gtk.Label composer_label { + styles ["composer"] + halign: start; + wrap: true; + } + + Gtk.Label performances_label { + styles ["performances", "dim-label"] + halign: start; + wrap: true; + } + } + } +} \ No newline at end of file diff --git a/data/ui/tile.blp b/data/ui/tag_tile.blp similarity index 59% rename from data/ui/tile.blp rename to data/ui/tag_tile.blp index 64022bb..67557b2 100644 --- a/data/ui/tile.blp +++ b/data/ui/tag_tile.blp @@ -1,7 +1,7 @@ using Gtk 4.0; using Adw 1; -template $MusicusTile : Gtk.FlowBoxChild { +template $MusicusTagTile : Gtk.FlowBoxChild { styles ["card", "activatable", "tile"] Gtk.Box { @@ -11,26 +11,16 @@ template $MusicusTile : Gtk.FlowBoxChild { Gtk.Label title_label { styles ["title"] halign: start; - label: _("Title"); + lines: 1; + ellipsize: end; } Gtk.Label subtitle_label { visible: false; styles ["subtitle", "dim-label"] halign: start; - label: _("Subtitle"); + lines: 1; + ellipsize: end; } } -} - -menu item_menu { - item { - label: _("_Play"); - } - item { - label: _("_Edit"); - } - item { - label: _("_Delete"); - } } \ No newline at end of file diff --git a/po/POTFILES b/po/POTFILES index a9ea4dd..de11f14 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,10 +1,11 @@ -data/res/home_page.blp -data/res/playlist_page.blp +data/ui/home_page.blp +data/ui/playlist_page.blp +data/ui/recording_tile.blp data/ui/search_entry.blp data/ui/search_tag.blp -data/res/tile.blp -data/res/welcome_page.blp -data/res/window.blp +data/ui/tag_tile.blp +data/ui/welcome_page.blp +data/ui/window.blp data/de.johrpan.musicus.desktop.in data/de.johrpan.musicus.appdata.xml.in data/de.johrpan.musicus.gschema.xml diff --git a/src/home_page.rs b/src/home_page.rs index 2b36df9..725b89d 100644 --- a/src/home_page.rs +++ b/src/home_page.rs @@ -1,9 +1,10 @@ use crate::{ library::{Ensemble, LibraryQuery, MusicusLibrary, Person, Recording, Work}, player::MusicusPlayer, + recording_tile::MusicusRecordingTile, search_entry::MusicusSearchEntry, search_tag::Tag, - tile::MusicusTile, + tag_tile::MusicusTagTile, }; use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*}; use gtk::{ @@ -170,31 +171,28 @@ impl MusicusHomePage { for composer in &results.composers { imp.composers_flow_box - .append(&MusicusTile::with_title(&composer.name_fl())); + .append(&MusicusTagTile::new(Tag::Composer(composer.clone()))); } for performer in &results.performers { imp.performers_flow_box - .append(&MusicusTile::with_title(&performer.name_fl())); + .append(&MusicusTagTile::new(Tag::Performer(performer.clone()))); } for ensemble in &results.ensembles { imp.ensembles_flow_box - .append(&MusicusTile::with_title(&ensemble.name)); + .append(&MusicusTagTile::new(Tag::Ensemble(ensemble.clone()))); } for work in &results.works { - imp.works_flow_box.append(&MusicusTile::with_subtitle( - &work.title, - &work.composer.name_fl(), - )); + imp.works_flow_box + .append(&MusicusTagTile::new(Tag::Work(work.clone()))); } for recording in &results.recordings { - imp.recordings_flow_box.append(&MusicusTile::with_subtitle( - &recording.work.title, - &recording.work.composer.name_fl(), - )); + let performances = self.library().performances(recording); + imp.recordings_flow_box + .append(&MusicusRecordingTile::new(recording, performances)); } imp.composers.replace(results.composers); diff --git a/src/library.rs b/src/library.rs index a9170ce..35c04b3 100644 --- a/src/library.rs +++ b/src/library.rs @@ -63,7 +63,7 @@ impl MusicusLibrary { .unwrap() .collect::>>() .unwrap(); - + let performers = self.con() .prepare("SELECT DISTINCT persons.id, persons.first_name, persons.last_name FROM persons INNER JOIN performances ON performances.person = persons.id WHERE persons.first_name LIKE ?1 OR persons.last_name LIKE ?1 LIMIT 9") .unwrap() @@ -167,7 +167,7 @@ impl MusicusLibrary { recordings, ..Default::default() } - }, + } LibraryQuery { composer: None, performer: Some(performer), @@ -196,7 +196,7 @@ impl MusicusLibrary { recordings, ..Default::default() } - }, + } LibraryQuery { composer: Some(composer), ensemble: Some(ensemble), @@ -216,7 +216,7 @@ impl MusicusLibrary { recordings, ..Default::default() } - }, + } LibraryQuery { composer: Some(composer), performer: Some(performer), @@ -236,10 +236,9 @@ impl MusicusLibrary { recordings, ..Default::default() } - }, + } LibraryQuery { - work: Some(work), - .. + work: Some(work), .. } => { let recordings = self .con() @@ -258,6 +257,28 @@ impl MusicusLibrary { } } + pub fn performances(&self, recording: &Recording) -> Vec { + let mut performances = self + .con() + .prepare("SELECT persons.id, persons.first_name, persons.last_name, instruments.id, instruments.name FROM performances INNER JOIN persons ON persons.id = performances.person LEFT JOIN instruments ON instruments.id = performances.role INNER JOIN recordings ON performances.recording = recordings.id WHERE recordings.id IS ?1") + .unwrap() + .query_map([&recording.id], Performance::from_person_row) + .unwrap() + .collect::>>() + .unwrap(); + + performances.append(&mut self + .con() + .prepare("SELECT ensembles.id, ensembles.name, instruments.id, instruments.name FROM performances INNER JOIN ensembles ON ensembles.id = performances.ensemble LEFT JOIN instruments ON instruments.id = performances.role INNER JOIN recordings ON performances.recording = recordings.id WHERE recordings.id IS ?1") + .unwrap() + .query_map([&recording.id], Performance::from_ensemble_row) + .unwrap() + .collect::>>() + .unwrap()); + + performances + } + fn con(&self) -> &Connection { self.imp().connection.get().unwrap() } @@ -394,3 +415,60 @@ impl Recording { }) } } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Performance { + Person(Person, Option), + Ensemble(Ensemble, Option), +} + +impl Performance { + pub fn from_person_row(row: &Row) -> rusqlite::Result { + let person = Person { + id: row.get(0)?, + first_name: row.get(1)?, + last_name: row.get(2)?, + }; + + Ok(match row.get::<_, Option>(3)? { + None => Self::Person(person, None), + Some(role_id) => Self::Person( + person, + Some(Role { + id: role_id, + name: row.get(4)?, + }), + ), + }) + } + + pub fn from_ensemble_row(row: &Row) -> rusqlite::Result { + let ensemble = Ensemble { + id: row.get(0)?, + name: row.get(1)?, + }; + + Ok(match row.get::<_, Option>(2)? { + None => Self::Ensemble(ensemble, None), + Some(role_id) => Self::Ensemble( + ensemble, + Some(Role { + id: role_id, + name: row.get(3)?, + }), + ), + }) + } +} + +#[derive(Debug, Clone, Eq)] +pub struct Role { + pub id: String, + pub name: String, +} + +impl PartialEq for Role { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} diff --git a/src/main.rs b/src/main.rs index d0dd28c..e224381 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,10 @@ mod home_page; mod library; mod player; mod playlist_page; +mod recording_tile; mod search_entry; mod search_tag; -mod tile; +mod tag_tile; mod welcome_page; mod window; diff --git a/src/recording_tile.rs b/src/recording_tile.rs new file mode 100644 index 0000000..6c05f09 --- /dev/null +++ b/src/recording_tile.rs @@ -0,0 +1,86 @@ +use crate::library::{Performance, Recording}; +use gtk::{glib, subclass::prelude::*}; +use std::cell::OnceCell; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/recording_tile.blp")] + pub struct MusicusRecordingTile { + #[template_child] + pub composer_label: TemplateChild, + #[template_child] + pub work_label: TemplateChild, + #[template_child] + pub performances_label: TemplateChild, + + pub recording: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for MusicusRecordingTile { + const NAME: &'static str = "MusicusRecordingTile"; + type Type = super::MusicusRecordingTile; + type ParentType = gtk::FlowBoxChild; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for MusicusRecordingTile {} + impl WidgetImpl for MusicusRecordingTile {} + impl FlowBoxChildImpl for MusicusRecordingTile {} +} + +glib::wrapper! { + pub struct MusicusRecordingTile(ObjectSubclass) + @extends gtk::Widget, gtk::FlowBoxChild; +} + +impl MusicusRecordingTile { + pub fn new(recording: &Recording, performances: Vec) -> Self { + let obj: Self = glib::Object::new(); + let imp = obj.imp(); + + imp.work_label.set_label(&recording.work.title); + imp.composer_label + .set_label(&recording.work.composer.name_fl()); + + imp.performances_label.set_label( + &performances + .into_iter() + .map(|performance| match performance { + Performance::Person(person, role) => { + let mut result = person.name_fl(); + if let Some(role) = role { + result.push_str(&format!(" ({})", role.name)); + } + result + } + Performance::Ensemble(ensemble, role) => { + let mut result = ensemble.name; + if let Some(role) = role { + result.push_str(&format!(" ({})", role.name)); + } + result + } + }) + .collect::>() + .join(", "), + ); + + imp.recording.set(recording.clone()).unwrap(); + + obj + } + + pub fn recording(&self) -> &Recording { + self.imp().recording.get().unwrap() + } +} diff --git a/src/search_entry.rs b/src/search_entry.rs index df0dc50..45e78c1 100644 --- a/src/search_entry.rs +++ b/src/search_entry.rs @@ -134,10 +134,11 @@ impl MusicusSearchEntry { } pub fn reset(&self) { - let mut tags = self.imp().tags.borrow_mut(); - - while let Some(tag) = tags.pop() { - self.imp().tags_box.remove(&tag); + { + let mut tags = self.imp().tags.borrow_mut(); + while let Some(tag) = tags.pop() { + self.imp().tags_box.remove(&tag); + } } self.imp().text.set_text(""); diff --git a/src/tag_tile.rs b/src/tag_tile.rs new file mode 100644 index 0000000..b17d855 --- /dev/null +++ b/src/tag_tile.rs @@ -0,0 +1,67 @@ +use crate::search_tag::Tag; +use gtk::{glib, prelude::*, subclass::prelude::*}; +use std::cell::OnceCell; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/tag_tile.blp")] + pub struct MusicusTagTile { + #[template_child] + pub title_label: TemplateChild, + #[template_child] + pub subtitle_label: TemplateChild, + + pub tag: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for MusicusTagTile { + const NAME: &'static str = "MusicusTagTile"; + type Type = super::MusicusTagTile; + type ParentType = gtk::FlowBoxChild; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for MusicusTagTile {} + impl WidgetImpl for MusicusTagTile {} + impl FlowBoxChildImpl for MusicusTagTile {} +} + +glib::wrapper! { + pub struct MusicusTagTile(ObjectSubclass) + @extends gtk::Widget, gtk::FlowBoxChild; +} + +impl MusicusTagTile { + pub fn new(tag: Tag) -> Self { + let obj: Self = glib::Object::new(); + let imp = obj.imp(); + + match &tag { + Tag::Composer(person) | Tag::Performer(person) => { + imp.title_label.set_label(&person.name_fl()); + } + Tag::Ensemble(ensemble) => { + imp.title_label.set_label(&ensemble.name); + } + Tag::Work(work) => { + imp.title_label.set_label(&work.title); + imp.subtitle_label.set_label(&work.composer.name_fl()); + imp.subtitle_label.set_visible(true); + } + } + + imp.tag.set(tag).unwrap(); + + obj + } +} diff --git a/src/tile.rs b/src/tile.rs deleted file mode 100644 index e862bca..0000000 --- a/src/tile.rs +++ /dev/null @@ -1,81 +0,0 @@ -use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*}; -use std::cell::RefCell; - -mod imp { - use super::*; - - #[derive(Properties, Debug, Default, gtk::CompositeTemplate)] - #[properties(wrapper_type = super::MusicusTile)] - #[template(file = "data/ui/tile.blp")] - pub struct MusicusTile { - #[property(get, set)] - pub title: RefCell, - - #[property(get, set)] - pub subtitle: RefCell>, - - #[template_child] - pub title_label: TemplateChild, - #[template_child] - pub subtitle_label: TemplateChild, - } - - #[glib::object_subclass] - impl ObjectSubclass for MusicusTile { - const NAME: &'static str = "MusicusTile"; - type Type = super::MusicusTile; - type ParentType = gtk::FlowBoxChild; - - fn class_init(klass: &mut Self::Class) { - klass.bind_template(); - } - - fn instance_init(obj: &glib::subclass::InitializingObject) { - obj.init_template(); - } - } - - #[glib::derived_properties] - impl ObjectImpl for MusicusTile { - fn constructed(&self) { - self.parent_constructed(); - - self.obj() - .bind_property("title", &self.title_label.get(), "label") - .sync_create() - .build(); - - self.obj() - .bind_property("subtitle", &self.subtitle_label.get(), "visible") - .sync_create() - .transform_to(|_, s: Option| Some(s.is_some())) - .build(); - - self.obj() - .bind_property("subtitle", &self.subtitle_label.get(), "label") - .sync_create() - .build(); - } - } - - impl WidgetImpl for MusicusTile {} - impl FlowBoxChildImpl for MusicusTile {} -} - -glib::wrapper! { - pub struct MusicusTile(ObjectSubclass) - @extends gtk::Widget, gtk::FlowBoxChild; -} - -impl MusicusTile { - pub fn with_title(title: &str) -> Self { - glib::Object::builder().property("title", title).build() - } - - pub fn with_subtitle(title: &str, subtitle: &str) -> Self { - glib::Object::builder() - .property("title", title) - .property("subtitle", subtitle) - .build() - } -}