Add functional query tags

This commit is contained in:
Elias Projahn 2023-10-08 00:16:41 +02:00
parent 9d4f37f601
commit 3c83452573
4 changed files with 217 additions and 29 deletions

View file

@ -1,7 +1,8 @@
use crate::{ use crate::{
library::{LibraryQuery, MusicusLibrary}, library::{Ensemble, LibraryQuery, MusicusLibrary, Person, Recording, Work},
player::MusicusPlayer, player::MusicusPlayer,
search_entry::MusicusSearchEntry, search_entry::MusicusSearchEntry,
search_tag::Tag,
tile::MusicusTile, tile::MusicusTile,
}; };
use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*}; use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*};
@ -9,7 +10,7 @@ use gtk::{
glib::{self, clone, Properties}, glib::{self, clone, Properties},
prelude::*, prelude::*,
}; };
use std::cell::OnceCell; use std::cell::{OnceCell, RefCell};
mod imp { mod imp {
use super::*; use super::*;
@ -24,6 +25,11 @@ mod imp {
#[property(get, construct_only)] #[property(get, construct_only)]
pub player: OnceCell<MusicusPlayer>, pub player: OnceCell<MusicusPlayer>,
pub persons: RefCell<Vec<Person>>,
pub ensembles: RefCell<Vec<Ensemble>>,
pub works: RefCell<Vec<Work>>,
pub recordings: RefCell<Vec<Recording>>,
#[template_child] #[template_child]
pub search_entry: TemplateChild<MusicusSearchEntry>, pub search_entry: TemplateChild<MusicusSearchEntry>,
#[template_child] #[template_child]
@ -106,7 +112,15 @@ impl MusicusHomePage {
#[template_callback] #[template_callback]
fn select(&self, search_entry: &MusicusSearchEntry) { fn select(&self, search_entry: &MusicusSearchEntry) {
search_entry.add_tag("Tag"); let imp = self.imp();
if let Some(person) = imp.persons.borrow().first() {
search_entry.add_tag(Tag::Person(person.clone()));
} else if let Some(ensemble) = imp.ensembles.borrow().first() {
search_entry.add_tag(Tag::Ensemble(ensemble.clone()));
} else if let Some(work) = imp.works.borrow().first() {
search_entry.add_tag(Tag::Work(work.clone()));
}
} }
fn query(&self, query: &LibraryQuery) { fn query(&self, query: &LibraryQuery) {
@ -137,25 +151,34 @@ impl MusicusHomePage {
imp.recordings_flow_box imp.recordings_flow_box
.set_visible(!results.recordings.is_empty()); .set_visible(!results.recordings.is_empty());
for person in results.persons { for person in &results.persons {
imp.persons_flow_box imp.persons_flow_box
.append(&MusicusTile::with_title(&person.name_fl())); .append(&MusicusTile::with_title(&person.name_fl()));
} }
for ensemble in results.ensembles { for ensemble in &results.ensembles {
imp.ensembles_flow_box imp.ensembles_flow_box
.append(&MusicusTile::with_title(&ensemble.name)); .append(&MusicusTile::with_title(&ensemble.name));
} }
for work in results.works { for work in &results.works {
imp.works_flow_box imp.works_flow_box.append(&MusicusTile::with_subtitle(
.append(&MusicusTile::with_subtitle(&work.title, &work.composer.name_fl())); &work.title,
&work.composer.name_fl(),
));
} }
for _recording in results.recordings { for recording in &results.recordings {
imp.recordings_flow_box imp.recordings_flow_box.append(&MusicusTile::with_subtitle(
.append(&MusicusTile::with_title("TODO")); &recording.work.title,
} &recording.work.composer.name_fl(),
));
}
imp.persons.replace(results.persons);
imp.ensembles.replace(results.ensembles);
imp.works.replace(results.works);
imp.recordings.replace(results.recordings);
} }
} }
} }

View file

@ -56,18 +56,86 @@ impl MusicusLibrary {
.. ..
} => { } => {
let persons = self.con() let persons = self.con()
.prepare("SELECT first_name, last_name FROM persons WHERE first_name LIKE ?1 OR last_name LIKE ?1 LIMIT 9") .prepare("SELECT id, first_name, last_name FROM persons WHERE first_name LIKE ?1 OR last_name LIKE ?1 LIMIT 9")
.unwrap() .unwrap()
.query_map([&search], Person::from_row) .query_map([&search], Person::from_row)
.unwrap() .unwrap()
.collect::<rusqlite::Result<Vec<Person>>>() .collect::<rusqlite::Result<Vec<Person>>>()
.unwrap(); .unwrap();
let ensembles = self
.con()
.prepare("SELECT id, name FROM ensembles WHERE name LIKE ?1 LIMIT 9")
.unwrap()
.query_map([&search], Ensemble::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Ensemble>>>()
.unwrap();
let works = self
.con()
.prepare("SELECT works.id, works.title, persons.id, persons.first_name, persons.last_name FROM works INNER JOIN persons ON works.composer = persons.id WHERE title LIKE ?1 LIMIT 9")
.unwrap()
.query_map([&search], Work::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Work>>>()
.unwrap();
LibraryResults { LibraryResults {
persons, persons,
ensembles,
works,
..Default::default() ..Default::default()
} }
} }
LibraryQuery {
person: Some(person),
ensemble: None,
work: None,
..
} => {
let persons = self.con()
.prepare("SELECT DISTINCT persons.id, persons.first_name, persons.last_name FROM persons INNER JOIN performances ON performances.person = persons.id INNER JOIN recordings ON recordings.id = performances.recording INNER JOIN works ON works.id = recordings.work WHERE works.composer IS ?1 AND (persons.first_name LIKE ?2 OR persons.last_name LIKE ?2) LIMIT 9")
.unwrap()
.query_map([&person.id, &search], Person::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Person>>>()
.unwrap();
let ensembles = self
.con()
.prepare("SELECT DISTINCT ensembles.id, ensembles.name FROM ensembles INNER JOIN performances ON performances.ensemble = ensembles.id INNER JOIN recordings ON recordings.id = performances.recording INNER JOIN works ON works.id = recordings.work WHERE works.composer IS ?1 AND ensembles.name LIKE ?2 LIMIT 9")
.unwrap()
.query_map([&person.id, &search], Ensemble::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Ensemble>>>()
.unwrap();
let works = self
.con()
.prepare("SELECT DISTINCT works.id, works.title, persons.id, persons.first_name, persons.last_name FROM works INNER JOIN persons ON works.composer = persons.id WHERE works.composer = ?1 AND title LIKE ?2 LIMIT 9")
.unwrap()
.query_map([&person.id, &search], Work::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Work>>>()
.unwrap();
let recordings = self
.con()
.prepare("SELECT DISTINCT recordings.id, works.id, works.title, persons.id, persons.first_name, persons.last_name FROM recordings INNER JOIN works ON recordings.work = works.id INNER JOIN persons ON works.composer = persons.id INNER JOIN performances ON recordings.id = performances.recording WHERE performances.person IS ?1 AND (works.title LIKE ?2 OR persons.first_name LIKE ?2 OR persons.last_name LIKE ?2) LIMIT 9")
.unwrap()
.query_map([&person.id, &search], Recording::from_row)
.unwrap()
.collect::<rusqlite::Result<Vec<Recording>>>()
.unwrap();
LibraryResults {
persons,
ensembles,
works,
recordings,
}
}
_ => LibraryResults::default(), _ => LibraryResults::default(),
} }
} }
@ -77,7 +145,7 @@ impl MusicusLibrary {
} }
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct LibraryQuery { pub struct LibraryQuery {
pub person: Option<Person>, pub person: Option<Person>,
pub ensemble: Option<Ensemble>, pub ensemble: Option<Ensemble>,
@ -85,7 +153,7 @@ pub struct LibraryQuery {
pub search: String, pub search: String,
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct LibraryResults { pub struct LibraryResults {
pub persons: Vec<Person>, pub persons: Vec<Person>,
pub ensembles: Vec<Ensemble>, pub ensembles: Vec<Ensemble>,
@ -95,11 +163,16 @@ pub struct LibraryResults {
impl LibraryResults { impl LibraryResults {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.persons.is_empty() && self.ensembles.is_empty() && self.works.is_empty() self.persons.is_empty()
&& self.ensembles.is_empty()
&& self.works.is_empty()
&& self.recordings.is_empty()
} }
} }
#[derive(Debug, Clone)]
pub struct Person { pub struct Person {
pub id: String,
pub first_name: String, pub first_name: String,
pub last_name: String, pub last_name: String,
} }
@ -107,8 +180,9 @@ pub struct Person {
impl Person { impl Person {
pub fn from_row(row: &Row) -> rusqlite::Result<Self> { pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self { Ok(Self {
first_name: row.get(0)?, id: row.get(0)?,
last_name: row.get(1)?, first_name: row.get(1)?,
last_name: row.get(2)?,
}) })
} }
@ -117,12 +191,61 @@ impl Person {
} }
} }
#[derive(Debug, Clone)]
pub struct Ensemble { pub struct Ensemble {
pub id: String,
pub name: String, pub name: String,
} }
pub struct Work { impl Ensemble {
pub title: String, pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
name: row.get(1)?,
})
}
} }
pub struct Recording {} #[derive(Debug, Clone)]
pub struct Work {
pub id: String,
pub title: String,
pub composer: Person,
}
impl Work {
pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
title: row.get(1)?,
composer: Person {
id: row.get(2)?,
first_name: row.get(3)?,
last_name: row.get(4)?,
},
})
}
}
#[derive(Debug, Clone)]
pub struct Recording {
pub id: String,
pub work: Work,
}
impl Recording {
pub fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
work: Work {
id: row.get(1)?,
title: row.get(2)?,
composer: Person {
id: row.get(3)?,
first_name: row.get(4)?,
last_name: row.get(5)?,
},
},
})
}
}

View file

@ -1,4 +1,7 @@
use crate::{library::LibraryQuery, search_tag::MusicusSearchTag}; use crate::{
library::LibraryQuery,
search_tag::{MusicusSearchTag, Tag},
};
use adw::{gdk, gio, glib, glib::clone, glib::subclass::Signal, prelude::*, subclass::prelude::*}; use adw::{gdk, gio, glib, glib::clone, glib::subclass::Signal, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{cell::RefCell, time::Duration}; use std::{cell::RefCell, time::Duration};
@ -140,20 +143,30 @@ impl MusicusSearchEntry {
self.imp().text.set_text(""); self.imp().text.set_text("");
} }
pub fn add_tag(&self, name: &str) { pub fn add_tag(&self, tag: Tag) {
self.imp().text.set_text(""); self.imp().text.set_text("");
let tag = MusicusSearchTag::new(name); let tag = MusicusSearchTag::new(tag);
self.imp().tags_box.append(&tag); self.imp().tags_box.append(&tag);
self.imp().tags.borrow_mut().push(tag); self.imp().tags.borrow_mut().push(tag);
} }
pub fn query(&self) -> LibraryQuery { pub fn query(&self) -> LibraryQuery {
LibraryQuery { let mut query = LibraryQuery {
search: self.imp().text.text().to_string(), search: self.imp().text.text().to_string(),
..Default::default() ..Default::default()
};
for tag in &*self.imp().tags.borrow() {
match tag.tag().clone() {
Tag::Person(person) => query.person = Some(person),
Tag::Ensemble(ensemble) => query.ensemble = Some(ensemble),
Tag::Work(work) => query.work = Some(work),
} }
} }
query
}
#[template_callback] #[template_callback]
fn activate(&self, _: &gtk::Text) { fn activate(&self, _: &gtk::Text) {
self.emit_by_name::<()>("activate", &[]); self.emit_by_name::<()>("activate", &[]);
@ -162,8 +175,15 @@ impl MusicusSearchEntry {
#[template_callback] #[template_callback]
fn backspace(&self, text: &gtk::Text) { fn backspace(&self, text: &gtk::Text) {
if text.cursor_position() == 0 { if text.cursor_position() == 0 {
if let Some(tag) = self.imp().tags.borrow_mut().pop() { let changed = if let Some(tag) = self.imp().tags.borrow_mut().pop() {
self.imp().tags_box.remove(&tag); self.imp().tags_box.remove(&tag);
true
} else {
false
};
if changed {
self.emit_by_name::<()>("query-changed", &[]);
} }
} }
} }

View file

@ -1,5 +1,7 @@
use crate::library::{Ensemble, Person, Work};
use adw::{glib, glib::subclass::Signal, prelude::*, subclass::prelude::*}; use adw::{glib, glib::subclass::Signal, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::cell::OnceCell;
mod imp { mod imp {
use super::*; use super::*;
@ -9,6 +11,7 @@ mod imp {
pub struct MusicusSearchTag { pub struct MusicusSearchTag {
#[template_child] #[template_child]
pub label: TemplateChild<gtk::Label>, pub label: TemplateChild<gtk::Label>,
pub tag: OnceCell<Tag>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -47,10 +50,22 @@ glib::wrapper! {
#[gtk::template_callbacks] #[gtk::template_callbacks]
impl MusicusSearchTag { impl MusicusSearchTag {
pub fn new(label: &str) -> Self { pub fn new(tag: Tag) -> Self {
let tag: MusicusSearchTag = glib::Object::new(); let obj: MusicusSearchTag = glib::Object::new();
tag.imp().label.set_label(label);
tag obj.imp().label.set_label(&match &tag {
Tag::Person(person) => person.name_fl(),
Tag::Ensemble(ensemble) => ensemble.name.clone(),
Tag::Work(work) => format!("{}: {}", &work.composer.name_fl(), &work.title),
});
obj.imp().tag.set(tag).unwrap();
obj
}
pub fn tag(&self) -> &Tag {
self.imp().tag.get().unwrap()
} }
#[template_callback] #[template_callback]
@ -58,3 +73,10 @@ impl MusicusSearchTag {
self.emit_by_name::<()>("remove", &[]); self.emit_by_name::<()>("remove", &[]);
} }
} }
#[derive(Debug, Clone)]
pub enum Tag {
Person(Person),
Ensemble(Ensemble),
Work(Work),
}