Inital library manager UI

This commit is contained in:
Elias Projahn 2025-01-17 09:38:00 +01:00
parent 38638d6fcd
commit f0135cd415
8 changed files with 1528 additions and 486 deletions

View file

@ -1,8 +1,9 @@
use std::{
cell::{OnceCell, RefCell},
path::{Path, PathBuf},
use crate::{
db::{self, models::*, schema::*, tables, TranslatedString},
program::Program,
};
use adw::gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use anyhow::Result;
use chrono::prelude::*;
use diesel::{
@ -12,11 +13,10 @@ use diesel::{
sql_types::BigInt,
QueryDsl, SqliteConnection,
};
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use crate::{
db::{self, models::*, schema::*, tables, TranslatedString},
program::Program,
use std::{
cell::{OnceCell, RefCell},
path::{Path, PathBuf},
};
diesel::define_sql_function! {
@ -537,6 +537,15 @@ impl MusicusLibrary {
Ok(persons)
}
pub fn all_persons(&self) -> Result<Vec<Person>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let persons = persons::table.order(persons::name).load(connection)?;
Ok(persons)
}
pub fn search_roles(&self, search: &str) -> Result<Vec<Role>> {
let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut();
@ -551,6 +560,15 @@ impl MusicusLibrary {
Ok(roles)
}
pub fn all_roles(&self) -> Result<Vec<Role>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let roles = roles::table.order(roles::name).load(connection)?;
Ok(roles)
}
pub fn search_instruments(&self, search: &str) -> Result<Vec<Instrument>> {
let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut();
@ -565,6 +583,17 @@ impl MusicusLibrary {
Ok(instruments)
}
pub fn all_instruments(&self) -> Result<Vec<Instrument>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let instruments = instruments::table
.order(instruments::name)
.load(connection)?;
Ok(instruments)
}
pub fn search_works(&self, composer: &Person, search: &str) -> Result<Vec<Work>> {
let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut();
@ -588,6 +617,20 @@ impl MusicusLibrary {
Ok(works)
}
pub fn all_works(&self) -> Result<Vec<Work>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let works = works::table
.order(works::name)
.load::<tables::Work>(connection)?
.into_iter()
.map(|w| Work::from_table(w, connection))
.collect::<Result<Vec<Work>>>()?;
Ok(works)
}
pub fn search_ensembles(&self, search: &str) -> Result<Vec<Ensemble>> {
let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut();
@ -611,6 +654,60 @@ impl MusicusLibrary {
Ok(ensembles)
}
pub fn all_ensembles(&self) -> Result<Vec<Ensemble>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let ensembles = ensembles::table
.order(ensembles::name)
.load::<tables::Ensemble>(connection)?
.into_iter()
.map(|e| Ensemble::from_table(e, connection))
.collect::<Result<Vec<Ensemble>>>()?;
Ok(ensembles)
}
pub fn all_recordings(&self) -> Result<Vec<Recording>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let recordings = recordings::table
.load::<tables::Recording>(connection)?
.into_iter()
.map(|e| Recording::from_table(e, connection))
.collect::<Result<Vec<Recording>>>()?;
Ok(recordings)
}
pub fn all_tracks(&self) -> Result<Vec<Track>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let tracks = tracks::table
.load::<tables::Track>(connection)?
.into_iter()
.map(|e| Track::from_table(e, connection))
.collect::<Result<Vec<Track>>>()?;
Ok(tracks)
}
pub fn all_mediums(&self) -> Result<Vec<tables::Medium>> {
// TODO
Ok(vec![])
}
pub fn all_albums(&self) -> Result<Vec<Album>> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let albums = albums::table.load::<Album>(connection)?;
Ok(albums)
}
pub fn composer_default_role(&self) -> Result<Role> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();

View file

@ -1,14 +1,20 @@
use adw::subclass::prelude::*;
use gtk::glib;
use std::cell::OnceCell;
use crate::{
editor::{
ensemble_editor::MusicusEnsembleEditor, instrument_editor::MusicusInstrumentEditor,
person_editor::MusicusPersonEditor, recording_editor::MusicusRecordingEditor,
role_editor::MusicusRoleEditor, work_editor::MusicusWorkEditor,
db::{
models::{Album, Ensemble, Instrument, Person, Recording, Role, Track, Work},
tables::Medium,
},
library::MusicusLibrary,
window::MusicusWindow,
};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::glib;
use std::{
cell::{OnceCell, RefCell},
ffi::OsStr,
path::Path,
};
mod imp {
@ -19,6 +25,37 @@ mod imp {
pub struct LibraryManager {
pub navigation: OnceCell<adw::NavigationView>,
pub library: OnceCell<MusicusLibrary>,
pub persons: RefCell<Vec<Person>>,
pub roles: RefCell<Vec<Role>>,
pub instruments: RefCell<Vec<Instrument>>,
pub works: RefCell<Vec<Work>>,
pub ensembles: RefCell<Vec<Ensemble>>,
pub recordings: RefCell<Vec<Recording>>,
pub tracks: RefCell<Vec<Track>>,
pub mediums: RefCell<Vec<Medium>>,
pub albums: RefCell<Vec<Album>>,
#[template_child]
pub library_path_row: TemplateChild<adw::ActionRow>,
#[template_child]
pub n_persons_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_roles_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_instruments_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_works_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_ensembles_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_recordings_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_tracks_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_mediums_label: TemplateChild<gtk::Label>,
#[template_child]
pub n_albums_label: TemplateChild<gtk::Label>,
}
#[glib::object_subclass]
@ -39,7 +76,13 @@ mod imp {
impl ObjectImpl for LibraryManager {}
impl WidgetImpl for LibraryManager {}
impl NavigationPageImpl for LibraryManager {}
impl NavigationPageImpl for LibraryManager {
fn showing(&self) {
self.parent_showing();
self.obj().update();
}
}
}
glib::wrapper! {
@ -60,99 +103,215 @@ impl LibraryManager {
}
#[template_callback]
fn add_person(&self, _: &gtk::Button) {
async fn open_library(&self, _: &adw::ActionRow) {
let dialog = gtk::FileDialog::builder()
.title(gettext("Select music library folder"))
.modal(true)
.build();
let root = self.root();
let window = root
.as_ref()
.and_then(|r| r.downcast_ref::<gtk::Window>())
.and_then(|w| w.downcast_ref::<MusicusWindow>())
.unwrap();
match dialog.select_folder_future(Some(window)).await {
Err(err) => {
if !err.matches(gtk::DialogError::Dismissed) {
log::error!("Folder selection failed: {err}");
}
}
Ok(folder) => window.set_library_folder(&folder),
}
}
#[template_callback]
fn import_archive(&self, _: &adw::ButtonRow) {}
#[template_callback]
fn export_archive(&self, _: &adw::ButtonRow) {}
#[template_callback]
fn show_persons(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_roles(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_instruments(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_works(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_ensembles(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_recordings(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_tracks(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_mediums(&self, _: &adw::ActionRow) {}
#[template_callback]
fn show_albums(&self, _: &adw::ActionRow) {}
// TODO: Make this async.
fn update(&self) {
let library = self.imp().library.get().unwrap();
if let Some(Some(filename)) = Path::new(&library.folder()).file_name().map(OsStr::to_str) {
self.imp().library_path_row.set_subtitle(filename);
}
let persons = library.all_persons().unwrap();
self.imp()
.navigation
.get()
.unwrap()
.push(&MusicusPersonEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
.n_persons_label
.set_label(&persons.len().to_string());
self.imp().persons.replace(persons);
#[template_callback]
fn add_role(&self, _: &gtk::Button) {
let roles = library.all_roles().unwrap();
self.imp().n_roles_label.set_label(&roles.len().to_string());
self.imp().roles.replace(roles);
let instruments = library.all_instruments().unwrap();
self.imp()
.navigation
.get()
.unwrap()
.push(&MusicusRoleEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
.n_instruments_label
.set_label(&instruments.len().to_string());
self.imp().instruments.replace(instruments);
#[template_callback]
fn add_instrument(&self, _: &gtk::Button) {
let works = library.all_works().unwrap();
self.imp().n_works_label.set_label(&works.len().to_string());
self.imp().works.replace(works);
let ensembles = library.all_ensembles().unwrap();
self.imp()
.navigation
.get()
.unwrap()
.push(&MusicusInstrumentEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
.n_ensembles_label
.set_label(&ensembles.len().to_string());
self.imp().ensembles.replace(ensembles);
#[template_callback]
fn add_work(&self, _: &gtk::Button) {
let recordings = library.all_recordings().unwrap();
self.imp()
.navigation
.get()
.unwrap()
.push(&MusicusWorkEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
.n_recordings_label
.set_label(&recordings.len().to_string());
self.imp().recordings.replace(recordings);
#[template_callback]
fn add_ensemble(&self, _: &gtk::Button) {
let tracks = library.all_tracks().unwrap();
self.imp()
.navigation
.get()
.unwrap()
.push(&MusicusEnsembleEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
.n_tracks_label
.set_label(&tracks.len().to_string());
self.imp().tracks.replace(tracks);
#[template_callback]
fn add_recording(&self, _: &gtk::Button) {
let mediums = library.all_mediums().unwrap();
self.imp()
.navigation
.get()
.unwrap()
.push(&MusicusRecordingEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
.n_mediums_label
.set_label(&mediums.len().to_string());
self.imp().mediums.replace(mediums);
let albums = library.all_albums().unwrap();
self.imp()
.n_albums_label
.set_label(&albums.len().to_string());
self.imp().albums.replace(albums);
}
#[template_callback]
fn add_medium(&self, _: &gtk::Button) {
todo!("Medium import");
}
// #[template_callback]
// fn add_person(&self, _: &gtk::Button) {
// self.imp()
// .navigation
// .get()
// .unwrap()
// .push(&MusicusPersonEditor::new(
// &self.imp().navigation.get().unwrap(),
// &self.imp().library.get().unwrap(),
// None,
// ));
// }
#[template_callback]
fn add_album(&self, _: &gtk::Button) {
todo!("Album editor");
// self.imp()
// .navigation
// .get()
// .unwrap()
// .push(&MusicusAlbumEditor::new(
// &self.imp().navigation.get().unwrap(),
// &self.imp().library.get().unwrap(),
// None,
// ));
}
// #[template_callback]
// fn add_role(&self, _: &gtk::Button) {
// 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, _: &gtk::Button) {
// 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, _: &gtk::Button) {
// 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, _: &gtk::Button) {
// 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, _: &gtk::Button) {
// 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, _: &gtk::Button) {
// todo!("Medium import");
// }
// #[template_callback]
// fn add_album(&self, _: &gtk::Button) {
// todo!("Album editor");
// // self.imp()
// // .navigation
// // .get()
// // .unwrap()
// // .push(&MusicusAlbumEditor::new(
// // &self.imp().navigation.get().unwrap(),
// // &self.imp().library.get().unwrap(),
// // None,
// // ));
// }
}

View file

@ -1,7 +1,6 @@
use std::{
cell::{Cell, OnceCell, RefCell},
path::PathBuf,
sync::Arc,
};
use fragile::Fragile;
@ -12,7 +11,6 @@ use gtk::{
prelude::*,
subclass::prelude::*,
};
use mpris_player::{Metadata, MprisPlayer, PlaybackStatus};
use once_cell::sync::Lazy;
use crate::{
@ -48,7 +46,7 @@ mod imp {
pub play: OnceCell<gstreamer_play::Play>,
pub play_signal_adapter: OnceCell<gstreamer_play::PlaySignalAdapter>,
pub mpris: OnceCell<Arc<MprisPlayer>>,
pub mpris: OnceCell<mpris_server::Player>,
}
impl MusicusPlayer {
@ -75,10 +73,22 @@ mod imp {
}
let item = item.downcast::<PlaylistItem>().unwrap();
self.mpris.get().unwrap().set_metadata(Metadata {
artist: Some(vec![item.make_title()]),
title: item.make_subtitle(),
..Default::default()
let obj = self.obj().clone();
let item_clone = item.clone();
glib::spawn_future_local(async move {
obj.imp()
.mpris
.get()
.unwrap()
.set_metadata(
mpris_server::Metadata::builder()
.artist(vec![item_clone.make_title()])
.title(item_clone.make_subtitle().unwrap_or_else(String::new))
.build(),
)
.await
.unwrap();
});
let uri = glib::filename_to_uri(item.path(), None)
@ -121,56 +131,13 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj().clone();
glib::spawn_future_local(async move {
obj.init_mpris().await;
});
let play = gstreamer_play::Play::new(None::<gstreamer_play::PlayVideoRenderer>);
let mpris = MprisPlayer::new(
config::APP_ID.to_owned(),
config::NAME.to_owned(),
config::APP_ID.to_owned(),
);
mpris.set_can_raise(true);
mpris.set_can_play(true);
mpris.set_can_pause(true);
mpris.set_can_go_previous(true);
mpris.set_can_go_next(true);
mpris.set_can_seek(false);
mpris.set_can_set_fullscreen(false);
let obj = self.obj();
mpris.connect_raise(clone!(
#[weak]
obj,
move || obj.emit_by_name::<()>("raise", &[])
));
mpris.connect_play(clone!(
#[weak]
obj,
move || obj.play()
));
mpris.connect_pause(clone!(
#[weak]
obj,
move || obj.pause()
));
mpris.connect_play_pause(clone!(
#[weak]
obj,
move || obj.play_pause()
));
mpris.connect_previous(clone!(
#[weak]
obj,
move || obj.previous()
));
mpris.connect_next(clone!(
#[weak]
obj,
move || obj.next()
));
self.mpris.set(mpris).expect("mpris should not be set");
let mut config = play.config();
config.set_position_update_interval(250);
play.set_config(config).unwrap();
@ -237,7 +204,11 @@ impl MusicusPlayer {
}
pub fn play_recording(&self, recording: &Recording) {
let tracks = &self.library().unwrap().tracks_for_recording(&recording.recording_id).unwrap();
let tracks = &self
.library()
.unwrap()
.tracks_for_recording(&recording.recording_id)
.unwrap();
if tracks.is_empty() {
log::warn!("Ignoring recording without tracks being added to the playlist.");
@ -330,20 +301,34 @@ impl MusicusPlayer {
let imp = self.imp();
imp.play.get().unwrap().play();
self.set_playing(true);
imp.mpris
.get()
.unwrap()
.set_playback_status(PlaybackStatus::Playing);
let obj = self.clone();
glib::spawn_future_local(async move {
obj.imp()
.mpris
.get()
.unwrap()
.set_playback_status(mpris_server::PlaybackStatus::Playing)
.await
.unwrap();
});
}
pub fn pause(&self) {
let imp = self.imp();
imp.play.get().unwrap().pause();
self.set_playing(false);
imp.mpris
.get()
.unwrap()
.set_playback_status(PlaybackStatus::Paused);
let obj = self.clone();
glib::spawn_future_local(async move {
obj.imp()
.mpris
.get()
.unwrap()
.set_playback_status(mpris_server::PlaybackStatus::Paused)
.await
.unwrap();
});
}
pub fn seek_to(&self, time_ms: u64) {
@ -378,6 +363,62 @@ impl MusicusPlayer {
}
}
async fn init_mpris(&self) {
let mpris = mpris_server::Player::builder(config::APP_ID)
.desktop_entry(config::APP_ID)
.can_raise(true)
.can_play(true)
.can_pause(true)
.can_go_previous(true)
.can_go_next(true)
.build()
.await
.unwrap();
let obj = self.clone();
mpris.connect_raise(clone!(
#[weak]
obj,
move |_| obj.emit_by_name::<()>("raise", &[])
));
mpris.connect_play(clone!(
#[weak]
obj,
move |_| obj.play()
));
mpris.connect_pause(clone!(
#[weak]
obj,
move |_| obj.pause()
));
mpris.connect_play_pause(clone!(
#[weak]
obj,
move |_| obj.play_pause()
));
mpris.connect_previous(clone!(
#[weak]
obj,
move |_| obj.previous()
));
mpris.connect_next(clone!(
#[weak]
obj,
move |_| obj.next()
));
self.imp()
.mpris
.set(mpris)
.expect("mpris should not be set");
}
fn generate_items(&self, program: &Program) {
if let Some(library) = self.library() {
// TODO: if program.play_full_recordings() {

View file

@ -6,7 +6,7 @@ use gtk::{
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
use std::cell::{Cell, OnceCell};
mod imp {
use super::*;
@ -16,7 +16,7 @@ mod imp {
#[template(file = "data/ui/player_bar.blp")]
pub struct PlayerBar {
#[property(get, construct_only)]
pub player: RefCell<MusicusPlayer>,
pub player: OnceCell<MusicusPlayer>,
pub seeking: Cell<bool>,
@ -42,7 +42,7 @@ mod imp {
impl PlayerBar {
fn update_item(&self) {
if let Some(item) = self.player.borrow().current_item() {
if let Some(item) = self.player.get().unwrap().current_item() {
self.title_label.set_label(&item.make_title());
if let Some(subtitle) = item.make_subtitle() {
@ -55,7 +55,7 @@ mod imp {
}
fn update_time(&self) {
let player = self.player.borrow();
let player = self.player.get().unwrap();
let current_time_ms = if self.seeking.get() {
(self.slider.value() * player.duration_ms() as f64) as u64
@ -106,7 +106,7 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
let player = self.player.borrow();
let player = self.player.get().unwrap();
player
.bind_property("playing", &self.play_button.get(), "icon-name")

View file

@ -1,14 +1,14 @@
use std::path::Path;
use adw::subclass::prelude::*;
use gtk::{gio, glib, glib::clone, prelude::*};
use crate::{
config, home_page::MusicusHomePage, library::MusicusLibrary, library_manager::LibraryManager,
player::MusicusPlayer, player_bar::PlayerBar, playlist_page::MusicusPlaylistPage,
welcome_page::MusicusWelcomePage,
};
use adw::subclass::prelude::*;
use gtk::{gio, glib, glib::clone, prelude::*};
use std::{cell::RefCell, path::Path};
mod imp {
use super::*;
@ -16,6 +16,7 @@ mod imp {
#[template(file = "data/ui/window.blp")]
pub struct MusicusWindow {
pub player: MusicusPlayer,
pub library_manager: RefCell<Option<LibraryManager>>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
@ -157,7 +158,7 @@ impl MusicusWindow {
}
#[template_callback]
fn set_library_folder(&self, folder: &gio::File) {
pub fn set_library_folder(&self, folder: &gio::File) {
let path = folder.path().unwrap();
let settings = gio::Settings::new(config::APP_ID);
@ -173,8 +174,16 @@ impl MusicusWindow {
self.imp().player.set_library(&library);
let navigation = self.imp().navigation_view.get();
if let Some(library_manager) = self.imp().library_manager.take() {
navigation.remove(&library_manager);
}
let library_manager = LibraryManager::new(&navigation, &library);
navigation
.replace(&[MusicusHomePage::new(&navigation, &library, &self.imp().player).into()]);
navigation.add(&LibraryManager::new(&navigation, &library));
navigation.add(&library_manager);
self.imp().library_manager.replace(Some(library_manager));
}
}