From fa94d61e1ee862903405a5e38fcd76ce6dc8edb9 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 16 Mar 2025 14:06:57 +0100 Subject: [PATCH] Add preferences for default program --- data/de.johrpan.Musicus.gschema.xml.in | 26 +++++- data/ui/preferences_dialog.blp | 65 +++++++++++++++ data/ui/search_page.blp | 2 +- data/ui/slider_row.blp | 41 ++++++++++ data/ui/welcome_page.blp | 2 +- src/library.rs | 4 +- src/main.rs | 2 + src/preferences_dialog.rs | 105 +++++++++++++++++++++++++ src/program.rs | 41 ++++++---- src/slider_row.rs | 89 +++++++++++++++++++++ src/window.rs | 18 +++-- 11 files changed, 367 insertions(+), 28 deletions(-) create mode 100644 data/ui/preferences_dialog.blp create mode 100644 data/ui/slider_row.blp create mode 100644 src/preferences_dialog.rs create mode 100644 src/slider_row.rs diff --git a/data/de.johrpan.Musicus.gschema.xml.in b/data/de.johrpan.Musicus.gschema.xml.in index a8bb49a..9848380 100644 --- a/data/de.johrpan.Musicus.gschema.xml.in +++ b/data/de.johrpan.Musicus.gschema.xml.in @@ -17,19 +17,39 @@ '' Path to the music library + + 20 + How much recently played items should be penalized (0–100) + + + 0 + How much recently added items should be preferred (0–100) + + + 60 + For how many minutes a composer should be penalized + + + 60 + For how many minutes an instrument should be penalized + + + true + Whether to play full recordings + - '{"title":"Just play some music","description":"Randomly select some music. Customize programs using the button in the top right.","design":"Program1","prefer_recently_added":0.0,"prefer_least_recently_played":0.1,"avoid_repeated_composers_seconds":3600,"avoid_repeated_instruments_seconds":3600,"play_full_recordings":true}' + '{"title":"Just play some music","description":"Randomly select some music. Customize programs using the button in the top right.","design":"Program1","prefer_recently_added":0.0,"prefer_least_recently_played":0.1,"avoid_repeated_composers":60,"avoid_repeated_instruments":60,"play_full_recordings":true}' Default settings for program 1 - '{"title":"What\'s new?","description":"Recordings that you recently added to your music library.","design":"Program2","prefer_recently_added":1.0,"prefer_least_recently_played":0.0,"avoid_repeated_composers_seconds":3600,"avoid_repeated_instruments_seconds":3600,"play_full_recordings":true}' + '{"title":"What\'s new?","description":"Recordings that you recently added to your music library.","design":"Program2","prefer_recently_added":1.0,"prefer_least_recently_played":0.0,"avoid_repeated_composers":60,"avoid_repeated_instruments":60,"play_full_recordings":true}' Default settings for program 2 - '{"title":"A long time ago","description":"Works that you haven\'t listened to for a long time.","design":"Program3","prefer_recently_added":0.0,"prefer_least_recently_played":1.0,"avoid_repeated_composers_seconds":3600,"avoid_repeated_instruments_seconds":3600,"play_full_recordings":true}' + '{"title":"A long time ago","description":"Works that you haven\'t listened to for a long time.","design":"Program3","prefer_recently_added":0.0,"prefer_least_recently_played":1.0,"avoid_repeated_composers":60,"avoid_repeated_instruments":60,"play_full_recordings":true}' Default settings for program 3 diff --git a/data/ui/preferences_dialog.blp b/data/ui/preferences_dialog.blp new file mode 100644 index 0000000..16f3dc9 --- /dev/null +++ b/data/ui/preferences_dialog.blp @@ -0,0 +1,65 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusPreferencesDialog: Adw.PreferencesDialog { + Adw.PreferencesPage { + title: _("Playback"); + + Adw.PreferencesGroup { + title: _("Default program"); + description: _("These settings apply when you add search results to the playlist."); + + $MusicusSliderRow { + title: _("Prefer recordings that haven't been played for a long time"); + suffix: _("%"); + + adjustment: Gtk.Adjustment prefer_least_recently_played_adjustment { + lower: 0; + upper: 100; + step-increment: 1; + page-increment: 10; + }; + } + + $MusicusSliderRow { + title: _("Prefer recordings that were recently added"); + suffix: _("%"); + + adjustment: Gtk.Adjustment prefer_recently_added_adjustment { + lower: 0; + upper: 100; + step-increment: 1; + page-increment: 10; + }; + } + + $MusicusSliderRow { + title: _("Avoid repeating composers"); + suffix: _(" min"); + + adjustment: Gtk.Adjustment avoid_repeated_composers_adjustment { + lower: 0; + upper: 120; + step-increment: 10; + page-increment: 30; + }; + } + + $MusicusSliderRow { + title: _("Avoid repeating instruments"); + suffix: _(" min"); + + adjustment: Gtk.Adjustment avoid_repeated_instruments_adjustment { + lower: 0; + upper: 120; + step-increment: 10; + page-increment: 30; + }; + } + + Adw.SwitchRow play_full_recordings_row { + title: _("Play full recordings"); + } + } + } +} diff --git a/data/ui/search_page.blp b/data/ui/search_page.blp index 7d5cc3c..d831e3c 100644 --- a/data/ui/search_page.blp +++ b/data/ui/search_page.blp @@ -269,7 +269,7 @@ menu primary_menu { item { label: _("_Preferences"); - action: "app.preferences"; + action: "win.preferences"; } item { diff --git a/data/ui/slider_row.blp b/data/ui/slider_row.blp new file mode 100644 index 0000000..8bc9360 --- /dev/null +++ b/data/ui/slider_row.blp @@ -0,0 +1,41 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusSliderRow: Adw.PreferencesRow { + activatable: false; + + Gtk.Box { + orientation: vertical; + spacing: 12; + margin-top: 12; + margin-bottom: 12; + margin-start: 12; + margin-end: 12; + + Gtk.Box { + spacing: 12; + + Gtk.Label { + label: bind template.title; + wrap: true; + xalign: 0.0; + hexpand: true; + } + + Gtk.Label value_label { + xalign: 1.0; + valign: center; + + styles [ + "numeric", + ] + } + } + + Gtk.Scale { + adjustment: bind template.adjustment; + hexpand: true; + valign: center; + } + } +} diff --git a/data/ui/welcome_page.blp b/data/ui/welcome_page.blp index 0ce8d2f..d3eefc4 100644 --- a/data/ui/welcome_page.blp +++ b/data/ui/welcome_page.blp @@ -32,7 +32,7 @@ template $MusicusWelcomePage : Adw.NavigationPage { menu primary_menu { item { label: _("_Preferences"); - action: "app.preferences"; + action: "win.preferences"; } item { label: _("_About Musicus"); diff --git a/src/library.rs b/src/library.rs index a7f511e..eaba4f5 100644 --- a/src/library.rs +++ b/src/library.rs @@ -629,14 +629,14 @@ impl Library { ( UNIXEPOCH('now', 'localtime') - UNIXEPOCH(instruments.last_played_at) ) * 1.0 / ") - .bind::(program.avoid_repeated_instruments_seconds()) + .bind::(program.avoid_repeated_instruments()) .sql(", 1.0 ), IFNULL( ( UNIXEPOCH('now', 'localtime') - UNIXEPOCH(persons.last_played_at) - ) * 1.0 / ").bind::(program.avoid_repeated_composers_seconds()).sql(", + ) * 1.0 / ").bind::(program.avoid_repeated_composers()).sql(", 1.0 ), 1.0 diff --git a/src/main.rs b/src/main.rs index 200d748..95295fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ mod player_bar; mod playlist_item; mod playlist_page; mod playlist_tile; +mod preferences_dialog; mod process; mod process_manager; mod process_row; @@ -20,6 +21,7 @@ mod recording_tile; mod search_page; mod search_tag; mod selector; +mod slider_row; mod tag_tile; mod util; mod welcome_page; diff --git a/src/preferences_dialog.rs b/src/preferences_dialog.rs new file mode 100644 index 0000000..aa40743 --- /dev/null +++ b/src/preferences_dialog.rs @@ -0,0 +1,105 @@ +use adw::{prelude::AdwDialogExt, subclass::prelude::*}; +use gtk::{gio, glib, prelude::*}; + +use crate::{config, slider_row::SliderRow}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/preferences_dialog.blp")] + pub struct PreferencesDialog { + #[template_child] + pub prefer_least_recently_played_adjustment: TemplateChild, + #[template_child] + pub prefer_recently_added_adjustment: TemplateChild, + #[template_child] + pub avoid_repeated_composers_adjustment: TemplateChild, + #[template_child] + pub avoid_repeated_instruments_adjustment: TemplateChild, + #[template_child] + pub play_full_recordings_row: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PreferencesDialog { + const NAME: &'static str = "MusicusPreferencesDialog"; + type Type = super::PreferencesDialog; + type ParentType = adw::PreferencesDialog; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + SliderRow::static_type(); + obj.init_template(); + } + } + + impl ObjectImpl for PreferencesDialog { + fn constructed(&self) { + self.parent_constructed(); + + let settings = gio::Settings::new(config::APP_ID); + + settings + .bind( + "prefer-least-recently-played", + &*self.prefer_least_recently_played_adjustment, + "value", + ) + .build(); + + settings + .bind( + "prefer-recently-added", + &*self.prefer_recently_added_adjustment, + "value", + ) + .build(); + + settings + .bind( + "avoid-repeated-composers", + &*self.avoid_repeated_composers_adjustment, + "value", + ) + .build(); + + settings + .bind( + "avoid-repeated-instruments", + &*self.avoid_repeated_instruments_adjustment, + "value", + ) + .build(); + + settings + .bind( + "play-full-recordings", + &*self.play_full_recordings_row, + "active", + ) + .build(); + } + } + + impl WidgetImpl for PreferencesDialog {} + impl AdwDialogImpl for PreferencesDialog {} + impl PreferencesDialogImpl for PreferencesDialog {} +} + +glib::wrapper! { + pub struct PreferencesDialog(ObjectSubclass) + @extends gtk::Widget, adw::Dialog, adw::PreferencesDialog; +} + +#[gtk::template_callbacks] +impl PreferencesDialog { + pub fn show(parent: &impl IsA) { + let obj: Self = glib::Object::new(); + obj.present(Some(parent)); + } +} diff --git a/src/program.rs b/src/program.rs index 804e71c..7a25482 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,10 +1,10 @@ use std::cell::{Cell, RefCell}; use anyhow::Result; -use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*}; +use gtk::{gio, glib, glib::Properties, prelude::*, subclass::prelude::*}; use serde::{Deserialize, Serialize}; -use crate::library::LibraryQuery; +use crate::{config, library::LibraryQuery}; mod imp { use super::*; @@ -47,10 +47,10 @@ mod imp { pub prefer_least_recently_played: Cell, #[property(get, set)] - pub avoid_repeated_composers_seconds: Cell, + pub avoid_repeated_composers: Cell, #[property(get, set)] - pub avoid_repeated_instruments_seconds: Cell, + pub avoid_repeated_instruments: Cell, #[property(get, set)] pub play_full_recordings: Cell, @@ -80,6 +80,8 @@ impl Program { } pub fn from_query(query: LibraryQuery) -> Self { + let settings = gio::Settings::new(&config::APP_ID); + glib::Object::builder() .property( "composer-id", @@ -92,25 +94,34 @@ impl Program { query.instrument.as_ref().map(|i| i.instrument_id.clone()), ) .property("work-id", query.work.as_ref().map(|w| w.work_id.clone())) - .property("prefer-recently-added", 0.0) - .property("prefer-least-recently-played", 0.5) .property( - "avoid-repeated-composers-seconds", + "prefer-recently-added", + settings.int("prefer-recently-added") as f64 / 100.0, + ) + .property( + "prefer-least-recently-played", + settings.int("prefer-least-recently-played") as f64 / 100.0, + ) + .property( + "avoid-repeated-composers", if query.composer.is_none() && query.work.is_none() { - 3600 + settings.int("avoid-repeated-composers") } else { 0 }, ) .property( - "avoid-repeated-instruments-seconds", + "avoid-repeated-instruments", if query.instrument.is_none() && query.work.is_none() { - 3600 + settings.int("avoid-repeated-instruments") } else { 0 }, ) - .property("play-full-recordings", true) + .property( + "play-full-recordings", + settings.boolean("play-full-recordings"), + ) .build() } @@ -127,12 +138,12 @@ impl Program { data.prefer_least_recently_played.get(), ) .property( - "avoid-repeated-composers-seconds", - data.avoid_repeated_composers_seconds.get(), + "avoid-repeated-composers", + data.avoid_repeated_composers.get(), ) .property( - "avoid-repeated-instruments-seconds", - data.avoid_repeated_instruments_seconds.get(), + "avoid-repeated-instruments", + data.avoid_repeated_instruments.get(), ) .property("play-full-recordings", data.play_full_recordings.get()) .build(); diff --git a/src/slider_row.rs b/src/slider_row.rs new file mode 100644 index 0000000..455f6bd --- /dev/null +++ b/src/slider_row.rs @@ -0,0 +1,89 @@ +use std::cell::RefCell; + +use adw::{prelude::*, subclass::prelude::*}; +use gtk::glib::{self, clone}; + +mod imp { + use super::*; + + #[derive(glib::Properties, gtk::CompositeTemplate, Debug, Default)] + #[properties(wrapper_type = super::SliderRow)] + #[template(file = "data/ui/slider_row.blp")] + pub struct SliderRow { + #[property(get, set)] + pub adjustment: RefCell, + + #[property(get, set)] + pub suffix: RefCell, + + #[template_child] + pub value_label: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SliderRow { + const NAME: &'static str = "MusicusSliderRow"; + type Type = super::SliderRow; + type ParentType = adw::PreferencesRow; + + 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 SliderRow { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj().to_owned(); + obj.connect_adjustment_notify(move |obj| { + obj.adjustment().connect_value_changed(clone!( + #[weak] + obj, + move |_| obj.update() + )); + + obj.update(); + }); + } + } + + impl WidgetImpl for SliderRow {} + impl ListBoxRowImpl for SliderRow {} + impl PreferencesRowImpl for SliderRow {} +} + +glib::wrapper! { + pub struct SliderRow(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow; +} + +#[gtk::template_callbacks] +impl SliderRow { + /// Create a new slider row. + /// + /// The adjustment can be used to control the range and initial value of the slider. Use the + /// adjustment's `value-changed` signal for getting updates. The current value is displayed + /// next to the slider followed by `suffix`. + pub fn new(title: &str, adjustment: >k::Adjustment, suffix: &str) -> Self { + glib::Object::builder() + .property("title", title) + .property("adjustment", adjustment) + .property("suffix", suffix) + .build() + } + + pub fn update(&self) { + self.imp().value_label.set_label(&format!( + "{:.0}{}", + self.adjustment().value(), + self.suffix() + )); + } +} diff --git a/src/window.rs b/src/window.rs index 2205af3..cbaefac 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,7 +1,8 @@ use std::{cell::RefCell, path::Path}; -use adw::subclass::prelude::*; -use gtk::{gio, glib, glib::clone, prelude::*}; +use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; +use gtk::{gio, glib, glib::clone}; use crate::{ config, @@ -11,15 +12,13 @@ use crate::{ player::Player, player_bar::PlayerBar, playlist_page::PlaylistPage, + preferences_dialog::PreferencesDialog, process_manager::ProcessManager, search_page::SearchPage, welcome_page::WelcomePage, }; mod imp { - use adw::prelude::{AlertDialogExt, AlertDialogExtManual}; - use gettextrs::gettext; - use super::*; #[derive(Debug, Default, gtk::CompositeTemplate)] @@ -89,8 +88,15 @@ mod imp { }) .build(); + let obj = self.obj().to_owned(); + let preferences_action = gio::ActionEntry::builder("preferences") + .activate(move |_, _, _| { + PreferencesDialog::show(&obj); + }) + .build(); + self.obj() - .add_action_entries([import_action, library_action]); + .add_action_entries([import_action, library_action, preferences_action]); let player_bar = PlayerBar::new(&self.player); self.player_bar_revealer.set_child(Some(&player_bar));