From cff489f43ec50c2ca33ac16cc94be2ab532278cd Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 2 Mar 2025 08:03:31 +0100 Subject: [PATCH] New search page --- data/ui/album_page.blp | 90 ++++ data/ui/search_entry.blp | 32 -- data/ui/{home_page.blp => search_page.blp} | 148 +++---- src/album_page.rs | 132 ++++++ src/db/models.rs | 23 +- src/home_page.rs | 389 ----------------- src/library.rs | 8 +- src/main.rs | 4 +- src/player.rs | 32 +- src/search_entry.rs | 267 ------------ src/search_page.rs | 478 +++++++++++++++++++++ src/window.rs | 20 +- 12 files changed, 830 insertions(+), 793 deletions(-) create mode 100644 data/ui/album_page.blp delete mode 100644 data/ui/search_entry.blp rename data/ui/{home_page.blp => search_page.blp} (77%) create mode 100644 src/album_page.rs delete mode 100644 src/home_page.rs delete mode 100644 src/search_entry.rs create mode 100644 src/search_page.rs diff --git a/data/ui/album_page.blp b/data/ui/album_page.blp new file mode 100644 index 0000000..6251197 --- /dev/null +++ b/data/ui/album_page.blp @@ -0,0 +1,90 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusAlbumPage: Adw.NavigationPage { + title: _("Album"); + + Adw.ToolbarView { + [top] + Adw.HeaderBar { + show-title: false; + } + + Gtk.ScrolledWindow { + Adw.Clamp { + Gtk.Box { + orientation: vertical; + margin-bottom: 24; + margin-start: 12; + margin-end: 12; + + Gtk.Box { + spacing: 12; + margin-top: 24; + + Gtk.Box { + orientation: vertical; + hexpand: true; + + Gtk.Label title_label { + wrap: true; + xalign: 0.0; + + styles [ + "title-1", + ] + } + + Gtk.Label subtitle_label { + wrap: true; + xalign: 0.0; + } + } + + Gtk.Button { + icon-name: "document-edit-symbolic"; + valign: center; + clicked => $edit_button_clicked() swapped; + + styles [ + "flat", + ] + } + + Gtk.Button { + icon-name: "media-playback-start-symbolic"; + label: _("_Play album"); + use-underline: true; + valign: center; + clicked => $play_button_clicked() swapped; + + styles [ + "pill", + "suggested-action", + ] + } + } + + Gtk.Label { + label: _("Recordings"); + xalign: 0; + margin-top: 24; + + styles [ + "heading", + ] + } + + Gtk.FlowBox recordings_flow_box { + margin-top: 12; + column-spacing: 12; + row-spacing: 12; + homogeneous: true; + selection-mode: none; + child-activated => $recording_selected() swapped; + } + } + } + } + } +} diff --git a/data/ui/search_entry.blp b/data/ui/search_entry.blp deleted file mode 100644 index 79cbfef..0000000 --- a/data/ui/search_entry.blp +++ /dev/null @@ -1,32 +0,0 @@ -using Gtk 4.0; - -template $MusicusSearchEntry : Gtk.Box { - styles ["searchbar"] - - margin-start: 12; - margin-end: 12; - margin-top: 6; - margin-bottom: 6; - - Gtk.Image { - icon-name: "system-search-symbolic"; - } - - Gtk.Box tags_box { - valign: center; - } - - Gtk.Text text { - placeholder-text: _("Enter composers, performers, works…"); - hexpand: true; - activate => $activate() swapped; - backspace => $backspace() swapped; - changed => $text_changed() swapped; - } - - Gtk.Image clear_icon { - visible: false; - icon-name: "edit-clear-symbolic"; - tooltip-text: _("Clear entry"); - } -} \ No newline at end of file diff --git a/data/ui/home_page.blp b/data/ui/search_page.blp similarity index 77% rename from data/ui/home_page.blp rename to data/ui/search_page.blp index 715bd9b..1ddba4b 100644 --- a/data/ui/home_page.blp +++ b/data/ui/search_page.blp @@ -1,98 +1,91 @@ using Gtk 4.0; using Adw 1; -template $MusicusHomePage: Adw.NavigationPage { +template $MusicusSearchPage: Adw.NavigationPage { title: _("Musicus"); - tag: "home"; - Gtk.Overlay { - Adw.ToolbarView { - [top] - Adw.HeaderBar header_bar { - [end] - MenuButton { - icon-name: "open-menu-symbolic"; - menu-model: primary_menu; - } + Adw.ToolbarView { + [top] + Adw.HeaderBar header_bar { + [end] + MenuButton { + icon-name: "open-menu-symbolic"; + menu-model: primary_menu; } + } - [top] + Gtk.ScrolledWindow scrolled_window { Adw.Clamp { maximum-size: 1000; tightening-threshold: 600; Gtk.Box { orientation: vertical; - - $MusicusSearchEntry search_entry { - activate => $select() swapped; - } + margin-bottom: 24; + margin-start: 12; + margin-end: 12; Gtk.Box header_box { visible: false; spacing: 12; - margin-start: 12; - margin-end: 12; margin-top: 24; - margin-bottom: 12; - - Gtk.Button { - styles [ - "flat" - ] - - valign: center; - icon-name: "go-previous-symbolic"; - clicked => $back_button_clicked() swapped; - } Gtk.Box { orientation: vertical; hexpand: true; Gtk.Label title_label { - styles [ - "title-1" - ] - + wrap: true; xalign: 0.0; + + styles [ + "title-1", + ] } Gtk.Label subtitle_label { + wrap: true; xalign: 0.0; } } Gtk.Button { - styles [ - "flat" - ] - - valign: center; icon-name: "document-edit-symbolic"; + valign: center; clicked => $edit_button_clicked() swapped; + + styles [ + "flat", + ] + } + + Gtk.Button { + icon-name: "media-playback-start-symbolic"; + label: _("_Play"); + use-underline: true; + valign: center; + clicked => $play_button_clicked() swapped; + + styles [ + "pill", + "suggested-action", + ] } } - } - } - Gtk.Stack stack { - Gtk.StackPage { - name: "results"; + Gtk.SearchEntry search_entry { + placeholder-text: _("Enter composers, performers, works…"); + margin-top: 24; + activate => $select() swapped; + } - child: Gtk.ScrolledWindow { - hscrollbar-policy: never; + Gtk.Stack stack { + Gtk.StackPage { + name: "results"; - Adw.Clamp { - maximum-size: 1000; - tightening-threshold: 600; - - Gtk.Box { + child: Gtk.Box { orientation: vertical; - margin-start: 12; - margin-end: 12; margin-top: 24; - margin-bottom: 68; Gtk.FlowBox programs_flow_box { margin-top: 12; @@ -106,7 +99,7 @@ template $MusicusHomePage: Adw.NavigationPage { Gtk.Label { styles [ - "heading" + "heading", ] visible: bind composers_flow_box.visible; @@ -126,7 +119,7 @@ template $MusicusHomePage: Adw.NavigationPage { Gtk.Label { styles [ - "heading" + "heading", ] visible: bind performers_flow_box.visible; @@ -146,7 +139,7 @@ template $MusicusHomePage: Adw.NavigationPage { Gtk.Label { styles [ - "heading" + "heading", ] visible: bind ensembles_flow_box.visible; @@ -166,7 +159,7 @@ template $MusicusHomePage: Adw.NavigationPage { Gtk.Label { styles [ - "heading" + "heading", ] visible: bind instruments_flow_box.visible; @@ -186,7 +179,7 @@ template $MusicusHomePage: Adw.NavigationPage { Gtk.Label { styles [ - "heading" + "heading", ] visible: bind works_flow_box.visible; @@ -206,7 +199,7 @@ template $MusicusHomePage: Adw.NavigationPage { Gtk.Label { styles [ - "heading" + "heading", ] visible: bind recordings_flow_box.visible; @@ -226,7 +219,7 @@ template $MusicusHomePage: Adw.NavigationPage { Gtk.Label { styles [ - "heading" + "heading", ] visible: bind albums_flow_box.visible; @@ -243,37 +236,22 @@ template $MusicusHomePage: Adw.NavigationPage { selection-mode: none; child-activated => $album_selected() swapped; } - } + }; } - }; - } - Gtk.StackPage { - name: "empty"; + Gtk.StackPage { + name: "empty"; - child: Adw.StatusPage { - icon-name: "system-search-symbolic"; - title: _("Nothing Found"); - description: _("Try a different search."); - }; + child: Adw.StatusPage { + icon-name: "system-search-symbolic"; + title: _("Nothing Found"); + description: _("Try a different search."); + }; + } + } } } } - - [overlay] - Gtk.Button play_button { - styles [ - "pill", - "suggested-action" - ] - - halign: end; - valign: end; - margin-end: 24; - margin-bottom: 24; - label: _("Play music"); - clicked => $play() swapped; - } } } diff --git a/src/album_page.rs b/src/album_page.rs new file mode 100644 index 0000000..3e15483 --- /dev/null +++ b/src/album_page.rs @@ -0,0 +1,132 @@ +use std::cell::OnceCell; + +use adw::subclass::prelude::*; +use gtk::{ + glib::{self, Properties}, + prelude::*, +}; + +use crate::{ + db::models::*, editor::album::AlbumEditor, library::Library, player::Player, + playlist_item::PlaylistItem, recording_tile::RecordingTile, +}; + +mod imp { + use super::*; + + #[derive(Properties, Debug, Default, gtk::CompositeTemplate)] + #[properties(wrapper_type = super::AlbumPage)] + #[template(file = "data/ui/album_page.blp")] + pub struct AlbumPage { + #[property(get, construct_only)] + pub navigation: OnceCell, + + #[property(get, construct_only)] + pub library: OnceCell, + + #[property(get, construct_only)] + pub player: OnceCell, + + pub album: OnceCell, + + #[template_child] + pub title_label: TemplateChild, + #[template_child] + pub subtitle_label: TemplateChild, + #[template_child] + pub recordings_flow_box: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AlbumPage { + const NAME: &'static str = "MusicusAlbumPage"; + type Type = super::AlbumPage; + 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) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for AlbumPage { + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl WidgetImpl for AlbumPage {} + impl NavigationPageImpl for AlbumPage {} +} + +glib::wrapper! { + pub struct AlbumPage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage; +} + +#[gtk::template_callbacks] +impl AlbumPage { + pub fn new( + navigation: &adw::NavigationView, + library: &Library, + player: &Player, + album: Album, + ) -> Self { + let obj: Self = glib::Object::builder() + .property("navigation", navigation) + .property("library", library) + .property("player", player) + .build(); + + obj.imp().title_label.set_label(&album.to_string()); + obj.imp().subtitle_label.set_label(&album.performers_string()); + + for recording in &album.recordings { + obj.imp() + .recordings_flow_box + .append(&RecordingTile::new(navigation, library, recording)); + } + + obj.imp().album.set(album).unwrap(); + + obj + } + + #[template_callback] + fn edit_button_clicked(&self) { + self.navigation().push(&AlbumEditor::new( + &self.navigation(), + &self.library(), + Some(&self.imp().album.get().unwrap().clone()), + )); + } + + #[template_callback] + fn play_button_clicked(&self) { + let playlist = self + .imp() + .album + .get() + .unwrap() + .recordings + .iter() + .map(|r| self.player().recording_to_playlist(r)) + .flatten() + .collect::>(); + + self.player().append_and_play(playlist); + } + + #[template_callback] + fn recording_selected(&self, tile: >k::FlowBoxChild) { + let playlist = self + .player() + .recording_to_playlist(tile.downcast_ref::().unwrap().recording()); + self.player().append_and_play(playlist); + } +} diff --git a/src/db/models.rs b/src/db/models.rs index cf78dd7..2b872c8 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -1,7 +1,7 @@ //! This module contains higher-level models combining information from //! multiple database tables. -use std::fmt::Display; +use std::{collections::HashSet, fmt::Display}; use anyhow::Result; use diesel::prelude::*; @@ -392,6 +392,27 @@ impl Album { recordings, }) } + + pub fn performers_string(&self) -> String { + let mut performers = HashSet::new(); + let mut ensembles = HashSet::new(); + + for recording in &self.recordings { + for performer in &recording.persons { + performers.insert(performer.to_string()); + } + + for ensemble in &recording.ensembles { + ensembles.insert(ensemble.to_string()); + } + } + + performers + .into_iter() + .chain(ensembles) + .collect::>() + .join(", ") + } } impl Eq for Album {} diff --git a/src/home_page.rs b/src/home_page.rs deleted file mode 100644 index 2e6e56e..0000000 --- a/src/home_page.rs +++ /dev/null @@ -1,389 +0,0 @@ -use std::cell::{OnceCell, RefCell}; - -use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*}; -use gtk::{ - gio, - glib::{self, Properties}, - prelude::*, -}; - -use crate::{ - album_tile::AlbumTile, - config, - db::models::*, - editor::{ - ensemble::EnsembleEditor, instrument::InstrumentEditor, person::PersonEditor, - work::WorkEditor, - }, - library::{Library, LibraryQuery}, - player::Player, - program::Program, - program_tile::ProgramTile, - recording_tile::RecordingTile, - search_entry::SearchEntry, - search_tag::Tag, - tag_tile::TagTile, -}; - -mod imp { - use super::*; - - #[derive(Properties, Debug, Default, gtk::CompositeTemplate)] - #[properties(wrapper_type = super::HomePage)] - #[template(file = "data/ui/home_page.blp")] - pub struct HomePage { - #[property(get, construct_only)] - pub navigation: OnceCell, - - #[property(get, construct_only)] - pub library: OnceCell, - - #[property(get, construct_only)] - pub player: OnceCell, - - pub programs: RefCell>, - pub composers: RefCell>, - pub performers: RefCell>, - pub ensembles: RefCell>, - pub instruments: RefCell>, - pub works: RefCell>, - pub recordings: RefCell>, - pub albums: RefCell>, - - #[template_child] - pub search_entry: TemplateChild, - #[template_child] - pub stack: TemplateChild, - #[template_child] - pub header_box: TemplateChild, - #[template_child] - pub title_label: TemplateChild, - #[template_child] - pub subtitle_label: TemplateChild, - #[template_child] - pub programs_flow_box: TemplateChild, - #[template_child] - pub composers_flow_box: TemplateChild, - #[template_child] - pub performers_flow_box: TemplateChild, - #[template_child] - pub ensembles_flow_box: TemplateChild, - #[template_child] - pub instruments_flow_box: TemplateChild, - #[template_child] - pub works_flow_box: TemplateChild, - #[template_child] - pub recordings_flow_box: TemplateChild, - #[template_child] - pub albums_flow_box: TemplateChild, - #[template_child] - pub play_button: TemplateChild, - } - - #[glib::object_subclass] - impl ObjectSubclass for HomePage { - const NAME: &'static str = "MusicusHomePage"; - type Type = super::HomePage; - 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) { - obj.init_template(); - } - } - - #[glib::derived_properties] - impl ObjectImpl for HomePage { - fn constructed(&self) { - self.parent_constructed(); - - self.search_entry.set_key_capture_widget(&*self.obj()); - - let obj = self.obj().to_owned(); - self.search_entry.connect_query_changed(move |entry| { - obj.query(&entry.query()); - }); - - let obj = self.obj().to_owned(); - self.library.get().unwrap().connect_changed(move |_| { - obj.imp().search_entry.reset(); - }); - - self.player - .get() - .unwrap() - .bind_property("active", &self.play_button.get(), "visible") - .invert_boolean() - .sync_create() - .build(); - - let settings = gio::Settings::new(&config::APP_ID); - - let programs = vec![ - Program::deserialize(&settings.string("program1")).unwrap(), - Program::deserialize(&settings.string("program2")).unwrap(), - Program::deserialize(&settings.string("program3")).unwrap(), - ]; - - for program in &programs { - self.programs_flow_box - .append(&ProgramTile::new(program.to_owned())); - } - - self.programs.replace(programs); - - self.obj().query(&LibraryQuery::default()); - } - } - - impl WidgetImpl for HomePage {} - impl NavigationPageImpl for HomePage {} -} - -glib::wrapper! { - pub struct HomePage(ObjectSubclass) - @extends gtk::Widget, adw::NavigationPage; -} - -#[gtk::template_callbacks] -impl HomePage { - pub fn new(navigation: &adw::NavigationView, library: &Library, player: &Player) -> Self { - glib::Object::builder() - .property("navigation", navigation) - .property("library", library) - .property("player", player) - .build() - } - - #[template_callback] - fn back_button_clicked(&self) { - self.imp().search_entry.reset(); - } - - #[template_callback] - fn edit_button_clicked(&self) { - if let Some(tag) = self.imp().search_entry.tags().first() { - match tag { - Tag::Composer(person) | Tag::Performer(person) => { - self.navigation().push(&PersonEditor::new( - &self.navigation(), - &self.library(), - Some(person), - )); - } - Tag::Ensemble(ensemble) => { - self.navigation().push(&EnsembleEditor::new( - &self.navigation(), - &self.library(), - Some(ensemble), - )); - } - Tag::Instrument(instrument) => self.navigation().push(&InstrumentEditor::new( - &self.navigation(), - &self.library(), - Some(instrument), - )), - Tag::Work(work) => self.navigation().push(&WorkEditor::new( - &self.navigation(), - &self.library(), - Some(work), - false, - )), - } - } - } - - #[template_callback] - fn play(&self) { - let program = Program::from_query(self.imp().search_entry.query()); - self.player().set_program(program); - - self.player().play(); - } - - #[template_callback] - fn select(&self, search_entry: &SearchEntry) { - let imp = self.imp(); - - if imp.programs_flow_box.is_visible() { - if let Some(program) = imp.programs.borrow().first().cloned() { - self.player().set_program(program); - } - } else { - let (composer, performer, ensemble, instrument, work, recording, album) = { - ( - imp.composers.borrow().first().cloned(), - imp.performers.borrow().first().cloned(), - imp.ensembles.borrow().first().cloned(), - imp.instruments.borrow().first().cloned(), - imp.works.borrow().first().cloned(), - imp.recordings.borrow().first().cloned(), - imp.albums.borrow().first().cloned(), - ) - }; - - if let Some(person) = composer { - search_entry.add_tag(Tag::Composer(person)); - } else if let Some(person) = performer { - search_entry.add_tag(Tag::Performer(person)); - } else if let Some(ensemble) = ensemble { - search_entry.add_tag(Tag::Ensemble(ensemble)); - } else if let Some(instrument) = instrument { - search_entry.add_tag(Tag::Instrument(instrument)); - } else if let Some(work) = work { - search_entry.add_tag(Tag::Work(work)); - } else if let Some(recording) = recording { - self.player().play_recording(&recording); - } else if let Some(album) = album { - self.show_album(&album); - } - } - } - - #[template_callback] - fn program_selected(&self, tile: >k::FlowBoxChild, _: >k::FlowBox) { - self.player() - .set_program(tile.downcast_ref::().unwrap().program()); - } - - #[template_callback] - fn tile_selected(&self, tile: >k::FlowBoxChild, _: >k::FlowBox) { - self.imp() - .search_entry - .add_tag(tile.downcast_ref::().unwrap().tag().clone()) - } - - #[template_callback] - fn recording_selected(&self, tile: >k::FlowBoxChild, _: >k::FlowBox) { - self.player() - .play_recording(tile.downcast_ref::().unwrap().recording()); - } - - #[template_callback] - fn album_selected(&self, tile: >k::FlowBoxChild, _: >k::FlowBox) { - self.show_album(tile.downcast_ref::().unwrap().album()); - } - - fn show_album(&self, _album: &Album) { - todo!("Show album"); - } - - fn query(&self, query: &LibraryQuery) { - let imp = self.imp(); - let results = self.library().query(query).unwrap(); - - for flowbox in [ - &imp.composers_flow_box, - &imp.performers_flow_box, - &imp.ensembles_flow_box, - &imp.instruments_flow_box, - &imp.works_flow_box, - &imp.recordings_flow_box, - &imp.albums_flow_box, - ] { - while let Some(widget) = flowbox.first_child() { - flowbox.remove(&widget); - } - } - - imp.programs_flow_box.set_visible(query.is_empty()); - - if let Some(tag) = imp.search_entry.tags().first() { - match tag { - Tag::Composer(person) | Tag::Performer(person) => { - imp.title_label.set_text(&person.name.get()); - imp.subtitle_label.set_visible(false); - } - Tag::Ensemble(ensemble) => { - imp.title_label.set_text(&ensemble.name.get()); - imp.subtitle_label.set_visible(false); - } - Tag::Instrument(instrument) => { - imp.title_label.set_text(&instrument.name.get()); - imp.subtitle_label.set_visible(false); - } - Tag::Work(work) => { - imp.title_label.set_text(&work.name.get()); - if let Some(composers) = work.composers_string() { - imp.subtitle_label.set_text(&composers); - imp.subtitle_label.set_visible(true); - } else { - imp.subtitle_label.set_visible(false); - } - } - } - - imp.header_box.set_visible(true); - } else { - imp.header_box.set_visible(false); - } - - if results.is_empty() { - imp.stack.set_visible_child_name("empty"); - } else { - imp.stack.set_visible_child_name("results"); - - imp.composers_flow_box - .set_visible(!results.composers.is_empty()); - imp.performers_flow_box - .set_visible(!results.performers.is_empty()); - imp.ensembles_flow_box - .set_visible(!results.ensembles.is_empty()); - imp.instruments_flow_box - .set_visible(!results.instruments.is_empty()); - imp.works_flow_box.set_visible(!results.works.is_empty()); - imp.recordings_flow_box - .set_visible(!results.recordings.is_empty()); - imp.albums_flow_box.set_visible(!results.albums.is_empty()); - - for composer in &results.composers { - imp.composers_flow_box - .append(&TagTile::new(Tag::Composer(composer.clone()))); - } - - for performer in &results.performers { - imp.performers_flow_box - .append(&TagTile::new(Tag::Performer(performer.clone()))); - } - - for ensemble in &results.ensembles { - imp.ensembles_flow_box - .append(&TagTile::new(Tag::Ensemble(ensemble.clone()))); - } - - for instrument in &results.instruments { - imp.instruments_flow_box - .append(&TagTile::new(Tag::Instrument(instrument.clone()))); - } - - for work in &results.works { - imp.works_flow_box - .append(&TagTile::new(Tag::Work(work.clone()))); - } - - for recording in &results.recordings { - imp.recordings_flow_box.append(&RecordingTile::new( - &self.navigation(), - &self.library(), - recording, - )); - } - - for album in &results.albums { - imp.albums_flow_box.append(&AlbumTile::new(album)); - } - - imp.composers.replace(results.composers); - imp.performers.replace(results.performers); - imp.ensembles.replace(results.ensembles); - imp.instruments.replace(results.instruments); - imp.works.replace(results.works); - imp.recordings.replace(results.recordings); - imp.albums.replace(results.albums); - } - } -} diff --git a/src/library.rs b/src/library.rs index 8e73803..17b3a9f 100644 --- a/src/library.rs +++ b/src/library.rs @@ -72,8 +72,8 @@ impl Library { .build() } - pub fn query(&self, query: &LibraryQuery) -> Result { - let search = format!("%{}%", query.search); + pub fn search(&self, query: &LibraryQuery, search: &str) -> Result { + let search = format!("%{}%", search); let mut binding = self.imp().connection.borrow_mut(); let connection = &mut *binding.as_mut().unwrap(); @@ -1541,14 +1541,13 @@ impl Library { } } -#[derive(Default, Debug)] +#[derive(Clone, Default, Debug)] pub struct LibraryQuery { pub composer: Option, pub performer: Option, pub ensemble: Option, pub instrument: Option, pub work: Option, - pub search: String, } impl LibraryQuery { @@ -1558,7 +1557,6 @@ impl LibraryQuery { && self.ensemble.is_none() && self.instrument.is_none() && self.work.is_none() - && self.search.is_empty() } } diff --git a/src/main.rs b/src/main.rs index fea094e..43ecac6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ +mod album_page; mod album_tile; mod application; mod config; mod db; mod editor; -mod home_page; +mod search_page; mod library; mod library_manager; mod player; @@ -14,7 +15,6 @@ mod playlist_tile; mod program; mod program_tile; mod recording_tile; -mod search_entry; mod search_tag; mod selector; mod tag_tile; diff --git a/src/player.rs b/src/player.rs index 02c8931..39f01d4 100644 --- a/src/player.rs +++ b/src/player.rs @@ -203,7 +203,7 @@ impl Player { }) } - pub fn play_recording(&self, recording: &Recording) { + pub fn recording_to_playlist(&self, recording: &Recording) -> Vec { let tracks = &self .library() .unwrap() @@ -211,8 +211,8 @@ impl Player { .unwrap(); if tracks.is_empty() { - log::warn!("Ignoring recording without tracks being added to the playlist."); - return; + log::warn!("Recording without tracks: {}.", &recording.recording_id); + return Vec::new(); } let performances = recording.performers_string(); @@ -272,14 +272,14 @@ impl Player { } } - self.append(items); + items } - pub fn append(&self, tracks: Vec) { + pub fn append(&self, items: Vec) { let playlist = self.playlist(); - for track in tracks { - playlist.append(&track); + for item in items { + playlist.append(&item); } if !self.active() && playlist.n_items() > 0 { @@ -289,6 +289,21 @@ impl Player { } } + pub fn append_and_play(&self, items: Vec) { + let playlist = self.playlist(); + let first_index = playlist.n_items(); + + for item in items { + playlist.append(&item); + } + + if playlist.n_items() > first_index { + self.set_active(true); + self.set_current_index(first_index); + self.play(); + } + } + pub fn play_pause(&self) { if self.playing() { self.pause(); @@ -423,7 +438,8 @@ impl Player { if let Some(library) = self.library() { // TODO: if program.play_full_recordings() { let recording = library.generate_recording(program).unwrap(); - self.play_recording(&recording); + let playlist = self.recording_to_playlist(&recording); + self.append(playlist); } } diff --git a/src/search_entry.rs b/src/search_entry.rs deleted file mode 100644 index a83be39..0000000 --- a/src/search_entry.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::{cell::RefCell, time::Duration}; - -use adw::{prelude::*, subclass::prelude::*}; -use gtk::{ - gdk, gio, - glib::{self, clone, subclass::Signal, Propagation}, -}; -use once_cell::sync::Lazy; - -use crate::{ - library::LibraryQuery, - search_tag::{SearchTag, Tag}, -}; - -mod imp { - use super::*; - - #[derive(Debug, Default, gtk::CompositeTemplate)] - #[template(file = "data/ui/search_entry.blp")] - pub struct SearchEntry { - #[template_child] - pub tags_box: TemplateChild, - #[template_child] - pub text: TemplateChild, - #[template_child] - pub clear_icon: TemplateChild, - - pub tags: RefCell>, - pub query_changed: RefCell>, - } - - #[glib::object_subclass] - impl ObjectSubclass for SearchEntry { - const NAME: &'static str = "MusicusSearchEntry"; - type Type = super::SearchEntry; - type ParentType = gtk::Box; - - fn class_init(klass: &mut Self::Class) { - klass.bind_template(); - klass.bind_template_instance_callbacks(); - klass.set_css_name("entry"); - - klass.add_shortcut( - >k::Shortcut::builder() - .trigger(>k::KeyvalTrigger::new( - gdk::Key::Escape, - gdk::ModifierType::empty(), - )) - .action(>k::CallbackAction::new(|widget, _| match widget - .downcast_ref::() - { - Some(obj) => { - obj.reset(); - Propagation::Stop - } - None => Propagation::Proceed, - })) - .build(), - ); - } - - fn instance_init(obj: &glib::subclass::InitializingObject) { - obj.init_template(); - } - } - - impl ObjectImpl for SearchEntry { - fn constructed(&self) { - let controller = gtk::GestureClick::new(); - - controller.connect_pressed(|gesture, _, _, _| { - gesture.set_state(gtk::EventSequenceState::Claimed); - }); - - let obj = self.obj().to_owned(); - controller.connect_released(move |_, _, _, _| { - obj.reset(); - }); - - self.clear_icon.add_controller(controller); - } - - fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = Lazy::new(|| { - vec![ - Signal::builder("activate").build(), - Signal::builder("query-changed").build(), - ] - }); - - SIGNALS.as_ref() - } - } - - impl WidgetImpl for SearchEntry { - fn grab_focus(&self) -> bool { - self.text.grab_focus_without_selecting() - } - } - - impl BoxImpl for SearchEntry {} -} - -glib::wrapper! { - pub struct SearchEntry(ObjectSubclass) - @extends gtk::Widget; -} - -#[gtk::template_callbacks] -impl SearchEntry { - pub fn new() -> Self { - glib::Object::new() - } - - pub fn connect_query_changed(&self, f: F) -> glib::SignalHandlerId { - self.connect_local("query-changed", true, move |values| { - let obj = values[0].get::().unwrap(); - f(&obj); - None - }) - } - - pub fn set_key_capture_widget(&self, widget: &impl IsA) { - let controller = gtk::EventControllerKey::new(); - - controller.connect_key_pressed(clone!( - #[weak(rename_to = this)] - self, - #[upgrade_or] - glib::Propagation::Proceed, - move |controller, _, _, _| { - match controller.forward(&this.imp().text.get()) { - true => { - this.grab_focus(); - glib::Propagation::Stop - } - false => glib::Propagation::Proceed, - } - } - )); - - controller.connect_key_released(clone!( - #[weak(rename_to = this)] - self, - move |controller, _, _, _| { - controller.forward(&this.imp().text.get()); - } - )); - - widget.add_controller(controller); - } - - pub fn reset(&self) { - { - 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(""); - self.emit_by_name::<()>("query-changed", &[]); - } - - pub fn add_tag(&self, tag: Tag) { - let imp = self.imp(); - - imp.clear_icon.set_visible(true); - imp.text.set_text(""); - - let tag = SearchTag::new(tag); - - tag.connect_remove(clone!( - #[weak(rename_to = this)] - self, - move |tag| { - let imp = this.imp(); - - imp.tags_box.remove(tag); - - { - imp.tags.borrow_mut().retain(|t| t.tag() != tag.tag()); - } - - this.emit_by_name::<()>("query-changed", &[]); - } - )); - - imp.tags_box.append(&tag); - imp.tags.borrow_mut().push(tag); - self.emit_by_name::<()>("query-changed", &[]); - } - - pub fn tags(&self) -> Vec { - self.imp() - .tags - .borrow() - .iter() - .map(|t| t.tag().to_owned()) - .collect() - } - - pub fn query(&self) -> LibraryQuery { - let mut query = LibraryQuery { - search: self.imp().text.text().to_string(), - ..Default::default() - }; - - for tag in &*self.imp().tags.borrow() { - match tag.tag().clone() { - Tag::Composer(person) => query.composer = Some(person), - Tag::Performer(person) => query.performer = Some(person), - Tag::Ensemble(ensemble) => query.ensemble = Some(ensemble), - Tag::Instrument(instrument) => query.instrument = Some(instrument), - Tag::Work(work) => query.work = Some(work), - } - } - - query - } - - #[template_callback] - fn activate(&self, _: >k::Text) { - self.emit_by_name::<()>("activate", &[]); - } - - #[template_callback] - fn backspace(&self, text: >k::Text) { - if text.position() == 0 { - let changed = if let Some(tag) = self.imp().tags.borrow_mut().pop() { - self.imp().tags_box.remove(&tag); - true - } else { - false - }; - - if changed { - self.emit_by_name::<()>("query-changed", &[]); - } - } - } - - #[template_callback] - async fn text_changed(&self, text: >k::Text) { - let imp = self.imp(); - - if imp.tags.borrow().is_empty() { - imp.clear_icon.set_visible(!text.text().is_empty()); - } - - if let Some(cancellable) = imp.query_changed.borrow_mut().take() { - cancellable.cancel(); - } - - let cancellable = gio::Cancellable::new(); - imp.query_changed.replace(Some(cancellable.clone())); - - let _ = gio::CancellableFuture::new( - async { - glib::timeout_future(Duration::from_millis(150)).await; - self.emit_by_name::<()>("query-changed", &[]); - }, - cancellable, - ) - .await; - } -} diff --git a/src/search_page.rs b/src/search_page.rs new file mode 100644 index 0000000..1dabc5b --- /dev/null +++ b/src/search_page.rs @@ -0,0 +1,478 @@ +use std::cell::{OnceCell, RefCell}; + +use adw::subclass::{navigation_page::NavigationPageImpl, prelude::*}; +use formatx::formatx; +use gettextrs::gettext; +use gtk::{ + gio, + glib::{self, Properties}, + prelude::*, +}; + +use crate::{ + album_page::AlbumPage, + album_tile::AlbumTile, + config, + db::models::*, + editor::{ + ensemble::EnsembleEditor, instrument::InstrumentEditor, person::PersonEditor, + work::WorkEditor, + }, + library::{Library, LibraryQuery}, + player::Player, + program::Program, + program_tile::ProgramTile, + recording_tile::RecordingTile, + search_tag::Tag, + tag_tile::TagTile, +}; + +mod imp { + use super::*; + + #[derive(Properties, Debug, Default, gtk::CompositeTemplate)] + #[properties(wrapper_type = super::SearchPage)] + #[template(file = "data/ui/search_page.blp")] + pub struct SearchPage { + #[property(get, construct_only)] + pub navigation: OnceCell, + + #[property(get, construct_only)] + pub library: OnceCell, + + #[property(get, construct_only)] + pub player: OnceCell, + + pub query: OnceCell, + pub highlight: RefCell>, + + pub programs: RefCell>, + pub composers: RefCell>, + pub performers: RefCell>, + pub ensembles: RefCell>, + pub instruments: RefCell>, + pub works: RefCell>, + pub recordings: RefCell>, + pub albums: RefCell>, + + #[template_child] + pub scrolled_window: TemplateChild, + #[template_child] + pub header_bar: TemplateChild, + #[template_child] + pub search_entry: TemplateChild, + #[template_child] + pub stack: TemplateChild, + #[template_child] + pub header_box: TemplateChild, + #[template_child] + pub title_label: TemplateChild, + #[template_child] + pub subtitle_label: TemplateChild, + #[template_child] + pub programs_flow_box: TemplateChild, + #[template_child] + pub composers_flow_box: TemplateChild, + #[template_child] + pub performers_flow_box: TemplateChild, + #[template_child] + pub ensembles_flow_box: TemplateChild, + #[template_child] + pub instruments_flow_box: TemplateChild, + #[template_child] + pub works_flow_box: TemplateChild, + #[template_child] + pub recordings_flow_box: TemplateChild, + #[template_child] + pub albums_flow_box: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SearchPage { + const NAME: &'static str = "MusicusSearchPage"; + type Type = super::SearchPage; + 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) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for SearchPage { + fn constructed(&self) { + self.parent_constructed(); + + self.search_entry.set_key_capture_widget(Some(&*self.obj())); + + let obj = self.obj().to_owned(); + self.search_entry.connect_search_changed(move |entry| { + obj.imp().scrolled_window.vadjustment().set_value(0.0); + obj.search(&entry.text()); + }); + } + } + + impl WidgetImpl for SearchPage { + fn map(&self) { + self.parent_map(); + self.search_entry.grab_focus(); + } + } + + impl NavigationPageImpl for SearchPage {} +} + +glib::wrapper! { + pub struct SearchPage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage; +} + +#[gtk::template_callbacks] +impl SearchPage { + pub fn new( + navigation: &adw::NavigationView, + library: &Library, + player: &Player, + query: LibraryQuery, + ) -> Self { + let obj: Self = glib::Object::builder() + .property("navigation", navigation) + .property("library", library) + .property("player", player) + .build(); + + if query.is_empty() { + let settings = gio::Settings::new(&config::APP_ID); + + let programs = vec![ + Program::deserialize(&settings.string("program1")).unwrap(), + Program::deserialize(&settings.string("program2")).unwrap(), + Program::deserialize(&settings.string("program3")).unwrap(), + ]; + + for program in &programs { + obj.imp() + .programs_flow_box + .append(&ProgramTile::new(program.to_owned())); + } + + obj.imp().programs.replace(programs); + } + + obj.imp().query.set(query).unwrap(); + obj.search(""); + + obj + } + + #[template_callback] + fn edit_button_clicked(&self) { + if let Some(highlight) = &*self.imp().highlight.borrow() { + match highlight { + Tag::Composer(person) | Tag::Performer(person) => { + self.navigation().push(&PersonEditor::new( + &self.navigation(), + &self.library(), + Some(person), + )); + } + Tag::Ensemble(ensemble) => { + self.navigation().push(&EnsembleEditor::new( + &self.navigation(), + &self.library(), + Some(ensemble), + )); + } + Tag::Instrument(instrument) => self.navigation().push(&InstrumentEditor::new( + &self.navigation(), + &self.library(), + Some(instrument), + )), + Tag::Work(work) => self.navigation().push(&WorkEditor::new( + &self.navigation(), + &self.library(), + Some(work), + false, + )), + } + } + } + + #[template_callback] + fn play_button_clicked(&self) { + let program = Program::from_query(self.imp().query.get().unwrap().clone()); + self.player().set_program(program); + self.player().play(); + } + + #[template_callback] + fn select(&self) { + let imp = self.imp(); + + if imp.programs_flow_box.is_visible() { + if let Some(program) = imp.programs.borrow().first().cloned() { + self.player().set_program(program); + } + } else { + let mut new_query = self.imp().query.get().unwrap().clone(); + + let query_changed = if let Some(person) = imp.composers.borrow().first().cloned() { + new_query.composer = Some(person); + true + } else if let Some(person) = imp.performers.borrow().first().cloned() { + new_query.performer = Some(person); + true + } else if let Some(ensemble) = imp.ensembles.borrow().first().cloned() { + new_query.ensemble = Some(ensemble); + true + } else if let Some(instrument) = imp.instruments.borrow().first().cloned() { + new_query.instrument = Some(instrument); + true + } else if let Some(work) = imp.works.borrow().first().cloned() { + new_query.work = Some(work); + true + } else if let Some(recording) = imp.recordings.borrow().first().cloned() { + let playlist = self.player().recording_to_playlist(&recording); + self.player().append_and_play(playlist); + false + } else if let Some(album) = imp.albums.borrow().first().cloned() { + self.show_album(&album); + false + } else { + false + }; + + if query_changed { + self.navigation().push(&SearchPage::new( + &self.navigation(), + &self.library(), + &self.player(), + new_query, + )); + } + } + } + + #[template_callback] + fn program_selected(&self, tile: >k::FlowBoxChild) { + self.player() + .set_program(tile.downcast_ref::().unwrap().program()); + } + + #[template_callback] + fn tile_selected(&self, tile: >k::FlowBoxChild) { + let mut new_query = self.imp().query.get().unwrap().clone(); + match tile.downcast_ref::().unwrap().tag().clone() { + Tag::Composer(person) => new_query.composer = Some(person), + Tag::Performer(person) => new_query.performer = Some(person), + Tag::Ensemble(ensemble) => new_query.ensemble = Some(ensemble), + Tag::Instrument(instrument) => new_query.instrument = Some(instrument), + Tag::Work(work) => new_query.work = Some(work), + } + + self.navigation().push(&SearchPage::new( + &self.navigation(), + &self.library(), + &self.player(), + new_query, + )); + } + + #[template_callback] + fn recording_selected(&self, tile: >k::FlowBoxChild) { + let playlist = self + .player() + .recording_to_playlist(tile.downcast_ref::().unwrap().recording()); + self.player().append_and_play(playlist); + } + + #[template_callback] + fn album_selected(&self, tile: >k::FlowBoxChild) { + self.show_album(tile.downcast_ref::().unwrap().album()); + } + + fn show_album(&self, album: &Album) { + self.navigation().push(&AlbumPage::new( + &self.navigation(), + &self.library(), + &self.player(), + album.to_owned(), + )); + } + + fn search(&self, search: &str) { + let query = self.imp().query.get().unwrap(); + + let imp = self.imp(); + let results = self.library().search(query, search).unwrap(); + + for flowbox in [ + &imp.composers_flow_box, + &imp.performers_flow_box, + &imp.ensembles_flow_box, + &imp.instruments_flow_box, + &imp.works_flow_box, + &imp.recordings_flow_box, + &imp.albums_flow_box, + ] { + while let Some(widget) = flowbox.first_child() { + flowbox.remove(&widget); + } + } + + // Only show programs initially. + imp.programs_flow_box + .set_visible(query.is_empty() && search.is_empty()); + + imp.header_bar.set_show_title(query.is_empty()); + imp.header_box.set_visible(!query.is_empty()); + + let highlight = if let Some(work) = &query.work { + imp.title_label.set_text(&work.name.get()); + if let Some(composers) = work.composers_string() { + imp.subtitle_label.set_text(&composers); + imp.subtitle_label.set_visible(true); + } else { + imp.subtitle_label.set_visible(false); + } + Some(Tag::Work(work.to_owned())) + } else if let Some(person) = &query.composer { + imp.title_label.set_text(&person.name.get()); + imp.subtitle_label.set_visible(false); + Some(Tag::Composer(person.to_owned())) + } else if let Some(person) = &query.performer { + imp.title_label.set_text(&person.name.get()); + imp.subtitle_label.set_visible(false); + Some(Tag::Performer(person.to_owned())) + } else if let Some(ensemble) = &query.ensemble { + imp.title_label.set_text(&ensemble.name.get()); + imp.subtitle_label.set_visible(false); + Some(Tag::Ensemble(ensemble.to_owned())) + } else if let Some(instrument) = &query.instrument { + imp.title_label + .set_text(&formatx!(gettext("Music for {}"), &instrument.name.get()).unwrap()); + imp.subtitle_label.set_visible(false); + Some(Tag::Instrument(instrument.to_owned())) + } else { + None + }; + + if let Some(highlight) = &highlight { + if !matches!(highlight, Tag::Work(_)) { + let mut details = Vec::new(); + + match highlight { + Tag::Composer(_) => { + if let Some(instrument) = &query.instrument { + details.push(formatx!(gettext("Works with {}"), instrument).unwrap()); + } + + if let (Some(person), Some(ensemble)) = (&query.performer, &query.ensemble) + { + details.push( + formatx!(gettext("Performed by {} and {}"), person, ensemble) + .unwrap(), + ); + } else if let Some(person) = &query.performer { + details.push(formatx!(gettext("Performed by {}"), person).unwrap()); + } else if let Some(ensemble) = &query.ensemble { + details.push(formatx!(gettext("Performed by {}"), ensemble).unwrap()); + } + } + Tag::Performer(_) => { + if let Some(instrument) = &query.instrument { + details.push(formatx!(gettext("Works with {}"), instrument).unwrap()); + } + + if let Some(ensemble) = &query.ensemble { + details.push(formatx!(gettext("Performed with {}"), ensemble).unwrap()); + } + } + Tag::Ensemble(_) => { + if let Some(instrument) = &query.instrument { + details.push(formatx!(gettext("Works with {}"), instrument).unwrap()); + } + } + Tag::Instrument(_) => (), + // Already covered. + Tag::Work(_) => unreachable!(), + } + + imp.subtitle_label.set_visible(!details.is_empty()); + imp.subtitle_label.set_text(&details.join(", ")); + } + } + + imp.highlight.replace(highlight); + + if results.is_empty() { + imp.stack.set_visible_child_name("empty"); + } else { + imp.stack.set_visible_child_name("results"); + + imp.composers_flow_box + .set_visible(!results.composers.is_empty()); + imp.performers_flow_box + .set_visible(!results.performers.is_empty()); + imp.ensembles_flow_box + .set_visible(!results.ensembles.is_empty()); + imp.instruments_flow_box + .set_visible(!results.instruments.is_empty()); + imp.works_flow_box.set_visible(!results.works.is_empty()); + imp.recordings_flow_box + .set_visible(!results.recordings.is_empty()); + imp.albums_flow_box.set_visible(!results.albums.is_empty()); + + for composer in &results.composers { + imp.composers_flow_box + .append(&TagTile::new(Tag::Composer(composer.clone()))); + } + + for performer in &results.performers { + imp.performers_flow_box + .append(&TagTile::new(Tag::Performer(performer.clone()))); + } + + for ensemble in &results.ensembles { + imp.ensembles_flow_box + .append(&TagTile::new(Tag::Ensemble(ensemble.clone()))); + } + + for instrument in &results.instruments { + imp.instruments_flow_box + .append(&TagTile::new(Tag::Instrument(instrument.clone()))); + } + + for work in &results.works { + imp.works_flow_box + .append(&TagTile::new(Tag::Work(work.clone()))); + } + + for recording in &results.recordings { + imp.recordings_flow_box.append(&RecordingTile::new( + &self.navigation(), + &self.library(), + recording, + )); + } + + for album in &results.albums { + imp.albums_flow_box.append(&AlbumTile::new(album)); + } + + imp.composers.replace(results.composers); + imp.performers.replace(results.performers); + imp.ensembles.replace(results.ensembles); + imp.instruments.replace(results.instruments); + imp.works.replace(results.works); + imp.recordings.replace(results.recordings); + imp.albums.replace(results.albums); + } + } +} diff --git a/src/window.rs b/src/window.rs index b334e7a..88e0a85 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,9 +4,15 @@ use adw::subclass::prelude::*; use gtk::{gio, glib, glib::clone, prelude::*}; use crate::{ - config, editor::tracks::TracksEditor, home_page::HomePage, library::Library, - library_manager::LibraryManager, player::Player, player_bar::PlayerBar, - playlist_page::PlaylistPage, welcome_page::WelcomePage, + config, + editor::tracks::TracksEditor, + library::{Library, LibraryQuery}, + library_manager::LibraryManager, + player::Player, + player_bar::PlayerBar, + playlist_page::PlaylistPage, + search_page::SearchPage, + welcome_page::WelcomePage, }; mod imp { @@ -189,7 +195,13 @@ impl Window { self.imp().player.set_library(&library); let navigation = self.imp().navigation_view.get(); - navigation.replace(&[HomePage::new(&navigation, &library, &self.imp().player).into()]); + navigation.replace(&[SearchPage::new( + &navigation, + &library, + &self.imp().player, + LibraryQuery::default(), + ) + .into()]); self.imp().library.replace(Some(library)); }