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));