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

1047
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,10 +12,11 @@ diesel_migrations = "2.2"
fragile = "2" fragile = "2"
gettext-rs = { version = "0.7", features = ["gettext-system"] } gettext-rs = { version = "0.7", features = ["gettext-system"] }
gstreamer-play = "0.23" gstreamer-play = "0.23"
gtk = { package = "gtk4", version = "0.9", features = ["v4_12", "blueprint"] } gtk = { package = "gtk4", version = "0.9", features = ["v4_18", "blueprint"] }
glib = { version = "0.20", features = ["v2_84"] }
lazy_static = "1" lazy_static = "1"
log = "0.4" log = "0.4"
mpris-player = "0.6" mpris-server = "0.8"
once_cell = "1" once_cell = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View file

@ -1,7 +1,7 @@
using Gtk 4.0; using Gtk 4.0;
using Adw 1; using Adw 1;
template $MusicusLibraryManager : Adw.NavigationPage { template $MusicusLibraryManager: Adw.NavigationPage {
title: _("Music Library"); title: _("Music Library");
tag: "library"; tag: "library";
@ -9,48 +9,286 @@ template $MusicusLibraryManager : Adw.NavigationPage {
[top] [top]
Adw.HeaderBar {} Adw.HeaderBar {}
Gtk.Box { Gtk.ScrolledWindow {
orientation: vertical; Adw.Clamp {
spacing: 12; Gtk.Box {
orientation: vertical;
margin-bottom: 24;
margin-start: 12;
margin-end: 12;
Gtk.Button { Gtk.Label {
label: _("Add person"); label: _("Overview");
clicked => $add_person() swapped; xalign: 0;
} margin-top: 24;
Gtk.Button { styles [
label: _("Add role"); "heading"
clicked => $add_role() swapped; ]
} }
Gtk.Button { Gtk.ListBox {
label: _("Add instrument"); selection-mode: none;
clicked => $add_instrument() swapped; margin-top: 12;
}
Gtk.Button { styles [
label: _("Add work"); "boxed-list-separate"
clicked => $add_work() swapped; ]
}
Gtk.Button { Adw.ActionRow library_path_row {
label: _("Add ensemble"); title: _("Library path");
clicked => $add_ensemble() swapped; activatable: true;
} activated => $open_library() swapped;
Gtk.Button { styles [
label: _("Add recording"); "property"
clicked => $add_recording() swapped; ]
}
Gtk.Button { [suffix]
label: _("Add album"); Gtk.Image {
clicked => $add_album() swapped; icon-name: "document-edit-symbolic";
} }
}
Gtk.Button { Adw.ButtonRow {
label: _("Add medium"); title: _("Import from archive");
clicked => $add_medium() swapped; end-icon-name: "go-next-symbolic";
activated => $import_archive() swapped;
}
Adw.ButtonRow {
title: _("Export to archive");
end-icon-name: "go-next-symbolic";
activated => $export_archive() swapped;
}
}
Gtk.Label {
label: _("Contents");
xalign: 0;
margin-top: 24;
styles [
"heading"
]
}
Gtk.ListBox {
selection-mode: none;
margin-top: 12;
styles [
"boxed-list"
]
Adw.ActionRow {
title: _("Persons");
activatable: true;
activated => $show_persons() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_persons_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Roles");
activatable: true;
activated => $show_roles() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_roles_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Instruments");
activatable: true;
activated => $show_instruments() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_instruments_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Works");
activatable: true;
activated => $show_works() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_works_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Ensembles");
activatable: true;
activated => $show_ensembles() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_ensembles_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Recordings");
activatable: true;
activated => $show_recordings() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_recordings_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Tracks");
activatable: true;
activated => $show_tracks() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_tracks_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Mediums");
activatable: true;
activated => $show_mediums() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_mediums_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
Adw.ActionRow {
title: _("Albums");
activatable: true;
activated => $show_albums() swapped;
[suffix]
Gtk.Box {
spacing: 6;
Gtk.Label n_albums_label {
label: "0";
styles [
"numeric"
]
}
Gtk.Image {
icon-name: "go-next-symbolic";
}
}
}
}
}
} }
} }
} }

View file

@ -1,8 +1,9 @@
use std::{ use crate::{
cell::{OnceCell, RefCell}, db::{self, models::*, schema::*, tables, TranslatedString},
path::{Path, PathBuf}, program::Program,
}; };
use adw::gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use anyhow::Result; use anyhow::Result;
use chrono::prelude::*; use chrono::prelude::*;
use diesel::{ use diesel::{
@ -12,11 +13,10 @@ use diesel::{
sql_types::BigInt, sql_types::BigInt,
QueryDsl, SqliteConnection, QueryDsl, SqliteConnection,
}; };
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use crate::{ use std::{
db::{self, models::*, schema::*, tables, TranslatedString}, cell::{OnceCell, RefCell},
program::Program, path::{Path, PathBuf},
}; };
diesel::define_sql_function! { diesel::define_sql_function! {
@ -537,6 +537,15 @@ impl MusicusLibrary {
Ok(persons) 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>> { pub fn search_roles(&self, search: &str) -> Result<Vec<Role>> {
let search = format!("%{}%", search); let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut(); let mut binding = self.imp().connection.borrow_mut();
@ -551,6 +560,15 @@ impl MusicusLibrary {
Ok(roles) 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>> { pub fn search_instruments(&self, search: &str) -> Result<Vec<Instrument>> {
let search = format!("%{}%", search); let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut(); let mut binding = self.imp().connection.borrow_mut();
@ -565,6 +583,17 @@ impl MusicusLibrary {
Ok(instruments) 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>> { pub fn search_works(&self, composer: &Person, search: &str) -> Result<Vec<Work>> {
let search = format!("%{}%", search); let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut(); let mut binding = self.imp().connection.borrow_mut();
@ -588,6 +617,20 @@ impl MusicusLibrary {
Ok(works) 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>> { pub fn search_ensembles(&self, search: &str) -> Result<Vec<Ensemble>> {
let search = format!("%{}%", search); let search = format!("%{}%", search);
let mut binding = self.imp().connection.borrow_mut(); let mut binding = self.imp().connection.borrow_mut();
@ -611,6 +654,60 @@ impl MusicusLibrary {
Ok(ensembles) 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> { pub fn composer_default_role(&self) -> Result<Role> {
let mut binding = self.imp().connection.borrow_mut(); let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap(); 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::{ use crate::{
editor::{ db::{
ensemble_editor::MusicusEnsembleEditor, instrument_editor::MusicusInstrumentEditor, models::{Album, Ensemble, Instrument, Person, Recording, Role, Track, Work},
person_editor::MusicusPersonEditor, recording_editor::MusicusRecordingEditor, tables::Medium,
role_editor::MusicusRoleEditor, work_editor::MusicusWorkEditor,
}, },
library::MusicusLibrary, 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 { mod imp {
@ -19,6 +25,37 @@ mod imp {
pub struct LibraryManager { pub struct LibraryManager {
pub navigation: OnceCell<adw::NavigationView>, pub navigation: OnceCell<adw::NavigationView>,
pub library: OnceCell<MusicusLibrary>, 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] #[glib::object_subclass]
@ -39,7 +76,13 @@ mod imp {
impl ObjectImpl for LibraryManager {} impl ObjectImpl for LibraryManager {}
impl WidgetImpl 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! { glib::wrapper! {
@ -60,99 +103,215 @@ impl LibraryManager {
} }
#[template_callback] #[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() self.imp()
.navigation .n_persons_label
.get() .set_label(&persons.len().to_string());
.unwrap() self.imp().persons.replace(persons);
.push(&MusicusPersonEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
#[template_callback] let roles = library.all_roles().unwrap();
fn add_role(&self, _: &gtk::Button) { self.imp().n_roles_label.set_label(&roles.len().to_string());
self.imp().roles.replace(roles);
let instruments = library.all_instruments().unwrap();
self.imp() self.imp()
.navigation .n_instruments_label
.get() .set_label(&instruments.len().to_string());
.unwrap() self.imp().instruments.replace(instruments);
.push(&MusicusRoleEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
#[template_callback] let works = library.all_works().unwrap();
fn add_instrument(&self, _: &gtk::Button) { self.imp().n_works_label.set_label(&works.len().to_string());
self.imp().works.replace(works);
let ensembles = library.all_ensembles().unwrap();
self.imp() self.imp()
.navigation .n_ensembles_label
.get() .set_label(&ensembles.len().to_string());
.unwrap() self.imp().ensembles.replace(ensembles);
.push(&MusicusInstrumentEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
#[template_callback] let recordings = library.all_recordings().unwrap();
fn add_work(&self, _: &gtk::Button) {
self.imp() self.imp()
.navigation .n_recordings_label
.get() .set_label(&recordings.len().to_string());
.unwrap() self.imp().recordings.replace(recordings);
.push(&MusicusWorkEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
#[template_callback] let tracks = library.all_tracks().unwrap();
fn add_ensemble(&self, _: &gtk::Button) {
self.imp() self.imp()
.navigation .n_tracks_label
.get() .set_label(&tracks.len().to_string());
.unwrap() self.imp().tracks.replace(tracks);
.push(&MusicusEnsembleEditor::new(
&self.imp().navigation.get().unwrap(),
&self.imp().library.get().unwrap(),
None,
));
}
#[template_callback] let mediums = library.all_mediums().unwrap();
fn add_recording(&self, _: &gtk::Button) {
self.imp() self.imp()
.navigation .n_mediums_label
.get() .set_label(&mediums.len().to_string());
.unwrap() self.imp().mediums.replace(mediums);
.push(&MusicusRecordingEditor::new(
&self.imp().navigation.get().unwrap(), let albums = library.all_albums().unwrap();
&self.imp().library.get().unwrap(), self.imp()
None, .n_albums_label
)); .set_label(&albums.len().to_string());
self.imp().albums.replace(albums);
} }
#[template_callback] // #[template_callback]
fn add_medium(&self, _: &gtk::Button) { // fn add_person(&self, _: &gtk::Button) {
todo!("Medium import"); // self.imp()
} // .navigation
// .get()
// .unwrap()
// .push(&MusicusPersonEditor::new(
// &self.imp().navigation.get().unwrap(),
// &self.imp().library.get().unwrap(),
// None,
// ));
// }
#[template_callback] // #[template_callback]
fn add_album(&self, _: &gtk::Button) { // fn add_role(&self, _: &gtk::Button) {
todo!("Album editor"); // self.imp()
// self.imp() // .navigation
// .navigation // .get()
// .get() // .unwrap()
// .unwrap() // .push(&MusicusRoleEditor::new(
// .push(&MusicusAlbumEditor::new( // &self.imp().navigation.get().unwrap(),
// &self.imp().navigation.get().unwrap(), // &self.imp().library.get().unwrap(),
// &self.imp().library.get().unwrap(), // None,
// 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::{ use std::{
cell::{Cell, OnceCell, RefCell}, cell::{Cell, OnceCell, RefCell},
path::PathBuf, path::PathBuf,
sync::Arc,
}; };
use fragile::Fragile; use fragile::Fragile;
@ -12,7 +11,6 @@ use gtk::{
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
}; };
use mpris_player::{Metadata, MprisPlayer, PlaybackStatus};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::{ use crate::{
@ -48,7 +46,7 @@ mod imp {
pub play: OnceCell<gstreamer_play::Play>, pub play: OnceCell<gstreamer_play::Play>,
pub play_signal_adapter: OnceCell<gstreamer_play::PlaySignalAdapter>, pub play_signal_adapter: OnceCell<gstreamer_play::PlaySignalAdapter>,
pub mpris: OnceCell<Arc<MprisPlayer>>, pub mpris: OnceCell<mpris_server::Player>,
} }
impl MusicusPlayer { impl MusicusPlayer {
@ -75,10 +73,22 @@ mod imp {
} }
let item = item.downcast::<PlaylistItem>().unwrap(); let item = item.downcast::<PlaylistItem>().unwrap();
self.mpris.get().unwrap().set_metadata(Metadata {
artist: Some(vec![item.make_title()]), let obj = self.obj().clone();
title: item.make_subtitle(), let item_clone = item.clone();
..Default::default() 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) let uri = glib::filename_to_uri(item.path(), None)
@ -121,56 +131,13 @@ mod imp {
fn constructed(&self) { fn constructed(&self) {
self.parent_constructed(); 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 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(); let mut config = play.config();
config.set_position_update_interval(250); config.set_position_update_interval(250);
play.set_config(config).unwrap(); play.set_config(config).unwrap();
@ -237,7 +204,11 @@ impl MusicusPlayer {
} }
pub fn play_recording(&self, recording: &Recording) { 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() { if tracks.is_empty() {
log::warn!("Ignoring recording without tracks being added to the playlist."); log::warn!("Ignoring recording without tracks being added to the playlist.");
@ -330,20 +301,34 @@ impl MusicusPlayer {
let imp = self.imp(); let imp = self.imp();
imp.play.get().unwrap().play(); imp.play.get().unwrap().play();
self.set_playing(true); self.set_playing(true);
imp.mpris
.get() let obj = self.clone();
.unwrap() glib::spawn_future_local(async move {
.set_playback_status(PlaybackStatus::Playing); obj.imp()
.mpris
.get()
.unwrap()
.set_playback_status(mpris_server::PlaybackStatus::Playing)
.await
.unwrap();
});
} }
pub fn pause(&self) { pub fn pause(&self) {
let imp = self.imp(); let imp = self.imp();
imp.play.get().unwrap().pause(); imp.play.get().unwrap().pause();
self.set_playing(false); self.set_playing(false);
imp.mpris
.get() let obj = self.clone();
.unwrap() glib::spawn_future_local(async move {
.set_playback_status(PlaybackStatus::Paused); obj.imp()
.mpris
.get()
.unwrap()
.set_playback_status(mpris_server::PlaybackStatus::Paused)
.await
.unwrap();
});
} }
pub fn seek_to(&self, time_ms: u64) { 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) { fn generate_items(&self, program: &Program) {
if let Some(library) = self.library() { if let Some(library) = self.library() {
// TODO: if program.play_full_recordings() { // TODO: if program.play_full_recordings() {

View file

@ -6,7 +6,7 @@ use gtk::{
subclass::prelude::*, subclass::prelude::*,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, OnceCell};
mod imp { mod imp {
use super::*; use super::*;
@ -16,7 +16,7 @@ mod imp {
#[template(file = "data/ui/player_bar.blp")] #[template(file = "data/ui/player_bar.blp")]
pub struct PlayerBar { pub struct PlayerBar {
#[property(get, construct_only)] #[property(get, construct_only)]
pub player: RefCell<MusicusPlayer>, pub player: OnceCell<MusicusPlayer>,
pub seeking: Cell<bool>, pub seeking: Cell<bool>,
@ -42,7 +42,7 @@ mod imp {
impl PlayerBar { impl PlayerBar {
fn update_item(&self) { 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()); self.title_label.set_label(&item.make_title());
if let Some(subtitle) = item.make_subtitle() { if let Some(subtitle) = item.make_subtitle() {
@ -55,7 +55,7 @@ mod imp {
} }
fn update_time(&self) { fn update_time(&self) {
let player = self.player.borrow(); let player = self.player.get().unwrap();
let current_time_ms = if self.seeking.get() { let current_time_ms = if self.seeking.get() {
(self.slider.value() * player.duration_ms() as f64) as u64 (self.slider.value() * player.duration_ms() as f64) as u64
@ -106,7 +106,7 @@ mod imp {
fn constructed(&self) { fn constructed(&self) {
self.parent_constructed(); self.parent_constructed();
let player = self.player.borrow(); let player = self.player.get().unwrap();
player player
.bind_property("playing", &self.play_button.get(), "icon-name") .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::{ use crate::{
config, home_page::MusicusHomePage, library::MusicusLibrary, library_manager::LibraryManager, config, home_page::MusicusHomePage, library::MusicusLibrary, library_manager::LibraryManager,
player::MusicusPlayer, player_bar::PlayerBar, playlist_page::MusicusPlaylistPage, player::MusicusPlayer, player_bar::PlayerBar, playlist_page::MusicusPlaylistPage,
welcome_page::MusicusWelcomePage, welcome_page::MusicusWelcomePage,
}; };
use adw::subclass::prelude::*;
use gtk::{gio, glib, glib::clone, prelude::*};
use std::{cell::RefCell, path::Path};
mod imp { mod imp {
use super::*; use super::*;
@ -16,6 +16,7 @@ mod imp {
#[template(file = "data/ui/window.blp")] #[template(file = "data/ui/window.blp")]
pub struct MusicusWindow { pub struct MusicusWindow {
pub player: MusicusPlayer, pub player: MusicusPlayer,
pub library_manager: RefCell<Option<LibraryManager>>,
#[template_child] #[template_child]
pub stack: TemplateChild<gtk::Stack>, pub stack: TemplateChild<gtk::Stack>,
@ -157,7 +158,7 @@ impl MusicusWindow {
} }
#[template_callback] #[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 path = folder.path().unwrap();
let settings = gio::Settings::new(config::APP_ID); let settings = gio::Settings::new(config::APP_ID);
@ -173,8 +174,16 @@ impl MusicusWindow {
self.imp().player.set_library(&library); self.imp().player.set_library(&library);
let navigation = self.imp().navigation_view.get(); 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 navigation
.replace(&[MusicusHomePage::new(&navigation, &library, &self.imp().player).into()]); .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));
} }
} }