Compare commits

...

12 commits
v0.1.0 ... main

42 changed files with 873 additions and 319 deletions

1
Cargo.lock generated
View file

@ -1880,7 +1880,6 @@ dependencies = [
"glib",
"gstreamer-play",
"gtk4",
"lazy_static",
"libadwaita",
"log",
"mpris-server",

View file

@ -17,7 +17,6 @@ gettext-rs = { version = "0.7", features = ["gettext-system"] }
glib = { version = "0.20", features = ["v2_84"] }
gstreamer-play = "0.23"
gtk = { package = "gtk4", version = "0.9", features = ["v4_18", "blueprint"] }
lazy_static = "1"
log = "0.4"
mpris-server = "0.8"
once_cell = "1"

View file

@ -6,15 +6,12 @@
font-size: smaller;
}
.searchbar .searchtag {
background-color: alpha(currentColor, 0.1);
border-radius: 100px;
}
.searchbar .searchtag>button {
min-width: 24px;
min-height: 24px;
margin: 0px;
.rounded-entry {
border-radius: 999px;
padding-left: 12px;
padding-right: 12px;
padding-top: 3px;
padding-bottom: 3px;
}
.tile {

View file

@ -65,9 +65,15 @@ template $MusicusAlbumEditor: Adw.NavigationPage {
margin-top: 24;
styles [
"boxed-list",
"boxed-list-separate",
]
Adw.SwitchRow enable_updates_row {
title: _("Enable updates");
subtitle: _("Keep this item up to date with the online metadata library");
active: true;
}
Adw.ButtonRow save_row {
title: _("_Create album");
use-underline: true;

View file

@ -50,23 +50,32 @@ template $MusicusEmptyPage: Adw.NavigationPage {
}
menu primary_menu {
item {
label: _("_Import music");
action: "win.import";
section {
item {
label: _("_Import music");
action: "win.import";
}
item {
label: _("_Create album");
action: "win.create-album";
}
item {
label: _("_Library manager");
action: "win.library";
}
}
item {
label: _("_Library manager");
action: "win.library";
}
section {
item {
label: _("_Preferences");
action: "win.preferences";
}
item {
label: _("_Preferences");
action: "win.preferences";
}
item {
label: _("_About Musicus");
action: "app.about";
item {
label: _("_About Musicus");
action: "app.about";
}
}
}

View file

@ -78,6 +78,10 @@ template $MusicusSearchPage: Adw.NavigationPage {
placeholder-text: _("Enter composers, performers, works…");
margin-top: 24;
activate => $select() swapped;
styles [
"rounded-entry",
]
}
Gtk.Stack stack {
@ -260,24 +264,33 @@ template $MusicusSearchPage: Adw.NavigationPage {
}
menu primary_menu {
item {
label: _("_Import music");
action: "win.import";
section {
item {
label: _("_Import music");
action: "win.import";
}
item {
label: _("_Create album");
action: "win.create-album";
}
item {
label: _("_Library manager");
action: "win.library";
}
}
item {
label: _("_Library manager");
action: "win.library";
}
section {
item {
label: _("_Preferences");
action: "win.preferences";
}
item {
label: _("_Preferences");
action: "win.preferences";
}
item {
label: _("_About Musicus");
action: "app.about";
item {
label: _("_About Musicus");
action: "app.about";
}
}
}

View file

@ -1,22 +0,0 @@
using Gtk 4.0;
template $MusicusSearchTag : Gtk.Box {
styles ["searchtag"]
margin-start: 6;
margin-end: 6;
Gtk.Label label {
styles ["caption-heading"]
margin-start: 12;
margin-end: 6;
max-width-chars: 15;
ellipsize: end;
}
Gtk.Button button {
styles ["flat", "circular"]
icon-name: "window-close-symbolic";
clicked => $remove() swapped;
}
}

View file

@ -5,7 +5,7 @@
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable",
"org.freedesktop.Sdk.Extension.llvm18"
"org.freedesktop.Sdk.Extension.llvm20"
],
"command": "musicus",
"finish-args": [
@ -20,7 +20,7 @@
"--env=G_MESSAGES_DEBUG=none"
],
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm18/bin",
"append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm20/bin",
"build-args": [
"--share=network"
],
@ -31,17 +31,6 @@
}
},
"modules": [
{
"name": "blueprint-compiler",
"buildsystem": "meson",
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler.git",
"tag": "v0.16.0"
}
]
},
{
"name": "musicus",
"buildsystem": "meson",

View file

@ -5,7 +5,7 @@
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable",
"org.freedesktop.Sdk.Extension.llvm18"
"org.freedesktop.Sdk.Extension.llvm20"
],
"command": "musicus",
"finish-args": [
@ -20,7 +20,7 @@
"--env=G_MESSAGES_DEBUG=none"
],
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm18/bin",
"append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm20/bin",
"build-args": [
"--share=network"
],

View file

@ -0,0 +1,248 @@
CREATE TABLE persons_old (
person_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP,
enable_updates BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE roles_old (
role_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
enable_updates BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE instruments_old (
instrument_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP,
enable_updates BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE works_old (
work_id TEXT NOT NULL PRIMARY KEY,
parent_work_id TEXT REFERENCES works(work_id),
sequence_number INTEGER,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP,
enable_updates BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE ensembles_old (
ensemble_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP,
enable_updates BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE recordings_old (
recording_id TEXT NOT NULL PRIMARY KEY,
work_id TEXT NOT NULL REFERENCES works(work_id),
year INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP,
enable_updates BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE mediums_old (
medium_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
discid TEXT NOT NULL,
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
CREATE TABLE albums_old (
album_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
INSERT INTO persons_old (
person_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
)
SELECT person_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
FROM persons;
DROP TABLE persons;
ALTER TABLE persons_old
RENAME TO persons;
INSERT INTO roles_old (
role_id,
name,
created_at,
edited_at,
last_used_at,
enable_updates
)
SELECT role_id,
name,
created_at,
edited_at,
last_used_at,
enable_updates
FROM roles;
DROP TABLE roles;
ALTER TABLE roles_old
RENAME TO roles;
INSERT INTO instruments_old (
instrument_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
)
SELECT instrument_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
FROM instruments;
DROP TABLE instruments;
ALTER TABLE instruments_old
RENAME TO instruments;
INSERT INTO works_old (
work_id,
parent_work_id,
sequence_number,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
)
SELECT work_id,
parent_work_id,
sequence_number,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
FROM works;
DROP TABLE works;
ALTER TABLE works_old
RENAME TO works;
INSERT INTO ensembles_old (
ensemble_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
)
SELECT ensemble_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
FROM ensembles;
DROP TABLE ensembles;
ALTER TABLE ensembles_old
RENAME TO ensembles;
INSERT INTO recordings_old (
recording_id,
work_id,
year,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
)
SELECT recording_id,
work_id,
year,
created_at,
edited_at,
last_used_at,
last_played_at,
enable_updates
FROM recordings;
DROP TABLE recordings;
ALTER TABLE recordings_old
RENAME TO recordings;
INSERT INTO mediums_old (
medium_id,
discid,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT medium_id,
discid,
created_at,
edited_at,
last_used_at,
last_played_at
FROM mediums;
DROP TABLE mediums;
ALTER TABLE mediums_old
RENAME TO mediums;
INSERT INTO albums_old (
album_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT album_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
FROM albums;
DROP TABLE albums;
ALTER TABLE albums_old
RENAME TO albums;

View file

@ -0,0 +1,245 @@
CREATE TABLE persons_new (
person_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
name TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
CREATE TABLE roles_new (
role_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
name TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime'))
);
CREATE TABLE instruments_new (
instrument_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
name TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
CREATE TABLE works_new (
work_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
parent_work_id TEXT REFERENCES works(work_id),
sequence_number INTEGER,
name TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
CREATE TABLE ensembles_new (
ensemble_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
name TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
CREATE TABLE recordings_new (
recording_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
work_id TEXT NOT NULL REFERENCES works(work_id),
year INTEGER,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
CREATE TABLE mediums_new (
medium_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
discid TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
CREATE TABLE albums_new (
album_id TEXT NOT NULL PRIMARY KEY REFERENCES item_state(id),
name TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'user',
enable_updates BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
edited_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_used_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now', 'localtime')),
last_played_at TIMESTAMP
);
INSERT INTO persons_new (
person_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT person_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
FROM persons;
DROP TABLE persons;
ALTER TABLE persons_new
RENAME TO persons;
INSERT INTO roles_new (
role_id,
name,
created_at,
edited_at,
last_used_at
)
SELECT role_id,
name,
created_at,
edited_at,
last_used_at
FROM roles;
DROP TABLE roles;
ALTER TABLE roles_new
RENAME TO roles;
INSERT INTO instruments_new (
instrument_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT instrument_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
FROM instruments;
DROP TABLE instruments;
ALTER TABLE instruments_new
RENAME TO instruments;
INSERT INTO works_new (
work_id,
parent_work_id,
sequence_number,
name,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT work_id,
parent_work_id,
sequence_number,
name,
created_at,
edited_at,
last_used_at,
last_played_at
FROM works;
DROP TABLE works;
ALTER TABLE works_new
RENAME TO works;
INSERT INTO ensembles_new (
ensemble_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT ensemble_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
FROM ensembles;
DROP TABLE ensembles;
ALTER TABLE ensembles_new
RENAME TO ensembles;
INSERT INTO recordings_new (
recording_id,
work_id,
year,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT recording_id,
work_id,
year,
created_at,
edited_at,
last_used_at,
last_played_at
FROM recordings;
DROP TABLE recordings;
ALTER TABLE recordings_new
RENAME TO recordings;
INSERT INTO mediums_new (
medium_id,
discid,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT medium_id,
discid,
created_at,
edited_at,
last_used_at,
last_played_at
FROM mediums;
DROP TABLE mediums;
ALTER TABLE mediums_new
RENAME TO mediums;
INSERT INTO albums_new (
album_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
)
SELECT album_id,
name,
created_at,
edited_at,
last_used_at,
last_played_at
FROM albums;
DROP TABLE albums;
ALTER TABLE albums_new
RENAME TO albums;

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-27 17:54+0200\n"
"POT-Creation-Date: 2025-05-30 15:27+0200\n"
"PO-Revision-Date: 2025-04-27 18:23+0200\n"
"Last-Translator: elias@johrpan.de\n"
"Language-Team: German <translation-team-de@lists.sourceforge.net>\n"
@ -818,7 +818,7 @@ msgstr "Bibliothek exportieren"
msgid "Exporting music library to {}"
msgstr "Bibliothek wird nach {} exportiert"
#: src/library_manager.rs:234 src/window.rs:282
#: src/library_manager.rs:234 src/window.rs:305
msgid "Updating metadata"
msgstr "Metadaten werden aktualisiert"
@ -826,19 +826,23 @@ msgstr "Metadaten werden aktualisiert"
msgid "Updating music library"
msgstr "Musikbibliothek wird aktualisiert"
#: src/window.rs:167
#: src/window.rs:166
msgid "Currently playing music"
msgstr "Musik wird abgespielt"
#: src/window.rs:190
msgid "Close window?"
msgstr "Fenster schließen?"
#: src/window.rs:169
#: src/window.rs:192
msgid "There are ongoing processes that will be canceled."
msgstr "Es gibt laufende Prozesse, die abgebrochen werden."
#: src/window.rs:174
#: src/window.rs:197
msgid "Keep open"
msgstr "Nicht schließen"
#: src/window.rs:175
#: src/window.rs:198
msgid "Close window"
msgstr "Fenster schließen"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-27 17:54+0200\n"
"POT-Creation-Date: 2025-05-30 15:27+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -784,7 +784,7 @@ msgstr ""
msgid "Exporting music library to {}"
msgstr ""
#: src/library_manager.rs:234 src/window.rs:282
#: src/library_manager.rs:234 src/window.rs:305
msgid "Updating metadata"
msgstr ""
@ -792,18 +792,22 @@ msgstr ""
msgid "Updating music library"
msgstr ""
#: src/window.rs:167
#: src/window.rs:166
msgid "Currently playing music"
msgstr ""
#: src/window.rs:190
msgid "Close window?"
msgstr ""
#: src/window.rs:169
#: src/window.rs:192
msgid "There are ongoing processes that will be canceled."
msgstr ""
#: src/window.rs:174
#: src/window.rs:197
msgid "Keep open"
msgstr ""
#: src/window.rs:175
#: src/window.rs:198
msgid "Close window"
msgstr ""

View file

@ -72,8 +72,7 @@ mod imp {
.unwrap()
.recordings
.iter()
.map(|r| obj.player().recording_to_playlist(r))
.flatten()
.flat_map(|r| obj.player().recording_to_playlist(r))
.collect::<Vec<PlaylistItem>>();
if let Err(err) = obj.player().append(playlist) {
@ -165,8 +164,7 @@ impl AlbumPage {
.unwrap()
.recordings
.iter()
.map(|r| self.player().recording_to_playlist(r))
.flatten()
.flat_map(|r| self.player().recording_to_playlist(r))
.collect::<Vec<PlaylistItem>>();
self.player().append_and_play(playlist);

View file

@ -45,7 +45,7 @@ impl AlbumTile {
pub fn new(album: &Album) -> Self {
let obj: Self = glib::Object::new();
obj.imp().title_label.set_label(&album.name.get());
obj.imp().title_label.set_label(album.name.get());
obj.imp().album.set(album.clone()).unwrap();
obj

View file

@ -74,6 +74,7 @@ pub struct Album {
pub album_id: String,
pub name: TranslatedString,
pub recordings: Vec<Recording>,
pub enable_updates: bool,
}
impl Eq for Person {}
@ -433,6 +434,7 @@ impl Album {
album_id: data.album_id,
name: data.name,
recordings,
enable_updates: data.enable_updates,
})
}

View file

@ -20,6 +20,8 @@ diesel::table! {
albums (album_id) {
album_id -> Text,
name -> Text,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
@ -40,11 +42,12 @@ diesel::table! {
ensembles (ensemble_id) {
ensemble_id -> Text,
name -> Text,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
enable_updates -> Bool,
}
}
@ -52,11 +55,12 @@ diesel::table! {
instruments (instrument_id) {
instrument_id -> Text,
name -> Text,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
enable_updates -> Bool,
}
}
@ -64,6 +68,8 @@ diesel::table! {
mediums (medium_id) {
medium_id -> Text,
discid -> Text,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
@ -75,16 +81,17 @@ diesel::table! {
persons (person_id) {
person_id -> Text,
name -> Text,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
enable_updates -> Bool,
}
}
diesel::table! {
recording_ensembles (recording_id, ensemble_id) {
recording_ensembles (recording_id, ensemble_id, sequence_number) {
recording_id -> Text,
ensemble_id -> Text,
role_id -> Nullable<Text>,
@ -93,7 +100,7 @@ diesel::table! {
}
diesel::table! {
recording_persons (recording_id, person_id) {
recording_persons (recording_id, person_id, sequence_number) {
recording_id -> Text,
person_id -> Text,
role_id -> Nullable<Text>,
@ -107,11 +114,12 @@ diesel::table! {
recording_id -> Text,
work_id -> Text,
year -> Nullable<Integer>,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
enable_updates -> Bool,
}
}
@ -119,10 +127,11 @@ diesel::table! {
roles (role_id) {
role_id -> Text,
name -> Text,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
enable_updates -> Bool,
}
}
@ -158,7 +167,7 @@ diesel::table! {
}
diesel::table! {
work_persons (work_id, person_id) {
work_persons (work_id, person_id, sequence_number) {
work_id -> Text,
person_id -> Text,
role_id -> Nullable<Text>,
@ -172,11 +181,12 @@ diesel::table! {
parent_work_id -> Nullable<Text>,
sequence_number -> Nullable<Integer>,
name -> Text,
source -> Text,
enable_updates -> Bool,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
enable_updates -> Bool,
}
}

View file

@ -24,11 +24,12 @@ use super::{schema::*, TranslatedString};
pub struct Person {
pub person_id: String,
pub name: TranslatedString,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
pub enable_updates: bool,
}
#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)]
@ -37,10 +38,11 @@ pub struct Person {
pub struct Role {
pub role_id: String,
pub name: TranslatedString,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub enable_updates: bool,
}
#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)]
@ -49,11 +51,12 @@ pub struct Role {
pub struct Instrument {
pub instrument_id: String,
pub name: TranslatedString,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
@ -63,11 +66,12 @@ pub struct Work {
pub parent_work_id: Option<String>,
pub sequence_number: Option<i32>,
pub name: TranslatedString,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
@ -92,11 +96,12 @@ pub struct WorkInstrument {
pub struct Ensemble {
pub ensemble_id: String,
pub name: TranslatedString,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
@ -114,11 +119,12 @@ pub struct Recording {
pub recording_id: String,
pub work_id: String,
pub year: Option<i32>,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
pub enable_updates: bool,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
@ -168,6 +174,8 @@ pub struct TrackWork {
pub struct Medium {
pub medium_id: String,
pub discid: String,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
@ -179,6 +187,8 @@ pub struct Medium {
pub struct Album {
pub album_id: String,
pub name: TranslatedString,
pub source: Source,
pub enable_updates: bool,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
@ -256,3 +266,40 @@ impl AsRef<Path> for PathBufWrapper {
self.0.as_ref()
}
}
#[derive(AsExpression, FromSqlRow, Copy, Clone, Debug)]
#[diesel(sql_type = Text)]
pub enum Source {
Metadata,
User,
Import,
Unknown,
}
impl ToSql<Text, Sqlite> for Source {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result {
out.set_value(match self {
Source::Metadata => "metadata",
Source::User => "user",
Source::Import => "import",
Source::Unknown => "unknown",
});
Ok(IsNull::No)
}
}
impl<DB> FromSql<Text, DB> for Source
where
DB: Backend,
String: FromSql<Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
Ok(match String::from_sql(bytes)?.as_str() {
"metadata" => Source::Metadata,
"user" => Source::User,
"import" => Source::Import,
_ => Source::Unknown,
})
}
}

View file

@ -39,6 +39,8 @@ mod imp {
#[template_child]
pub recordings_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub enable_updates_row: TemplateChild<adw::SwitchRow>,
#[template_child]
pub save_row: TemplateChild<adw::ButtonRow>,
}
@ -126,6 +128,10 @@ impl AlbumEditor {
for recording in &album.recordings {
obj.add_recording(recording.to_owned());
}
obj.imp()
.enable_updates_row
.set_active(album.enable_updates);
}
obj
@ -191,10 +197,16 @@ impl AlbumEditor {
.map(|r| r.recording())
.collect::<Vec<Recording>>();
let enable_updates = self.imp().enable_updates_row.is_active();
if let Some(album_id) = self.imp().album_id.get() {
library.update_album(album_id, name, recordings).unwrap();
library
.update_album(album_id, name, recordings, enable_updates)
.unwrap();
} else {
let album = library.create_album(name, recordings).unwrap();
let album = library
.create_album(name, recordings, enable_updates)
.unwrap();
self.emit_by_name::<()>("created", &[&album]);
}

View file

@ -69,7 +69,7 @@ mod imp {
self.parent_constructed();
let set_design_action = gio::ActionEntry::builder("set-design")
.parameter_type(Some(&glib::VariantTy::STRING))
.parameter_type(Some(glib::VariantTy::STRING))
.state(glib::Variant::from("program-1"))
.build();

View file

@ -246,7 +246,7 @@ impl RecordingEditor {
}
fn set_work(&self, work: Work) {
self.imp().work_row.set_title(&work.name.get());
self.imp().work_row.set_title(work.name.get());
self.imp().work_row.set_subtitle(
&work
.composers_string()

View file

@ -245,8 +245,7 @@ impl TracksEditor {
.track_rows
.borrow()
.iter()
.map(|t| t.track_data().parts.clone())
.flatten()
.flat_map(|t| t.track_data().parts.clone())
.collect::<Vec<Work>>()
};

View file

@ -390,17 +390,15 @@ impl WorkEditor {
};
self.emit_by_name::<()>("created", &[&part]);
} else if let Some(work_id) = self.imp().work_id.get() {
library
.update_work(work_id, name, parts, composers, instruments, enable_updates)
.unwrap();
} else {
if let Some(work_id) = self.imp().work_id.get() {
library
.update_work(work_id, name, parts, composers, instruments, enable_updates)
.unwrap();
} else {
let work = library
.create_work(name, parts, composers, instruments, enable_updates)
.unwrap();
self.emit_by_name::<()>("created", &[&work]);
}
let work = library
.create_work(name, parts, composers, instruments, enable_updates)
.unwrap();
self.emit_by_name::<()>("created", &[&work]);
}
self.imp().navigation.get().unwrap().pop();

View file

@ -145,7 +145,7 @@ impl WorkEditorPartRow {
}
fn set_part(&self, part: Work) {
self.set_title(&part.name.get());
self.set_title(part.name.get());
if !part.parts.is_empty() {
self.set_subtitle(

View file

@ -10,8 +10,8 @@ use gtk::{gio, glib, glib::subclass::Signal};
use once_cell::sync::Lazy;
use crate::{
config, library::Library, process::Process, process_manager::ProcessManager,
process_row::ProcessRow,
config, db::tables::Source, library::Library, process::Process,
process_manager::ProcessManager, process_row::ProcessRow,
};
mod imp {
@ -92,8 +92,8 @@ impl EmptyPage {
#[template_callback]
async fn download_library(&self) {
let dialog = adw::AlertDialog::builder()
.heading(&gettext("Disclaimer"))
.body(&gettext("You are about to download a library of audio files. These are from recordings that are in the public domain under EU law and are hosted on a server within the EU. Please ensure that you comply with the copyright laws of you country."))
.heading(gettext("Disclaimer"))
.body(gettext("You are about to download a library of audio files. These are from recordings that are in the public domain under EU law and are hosted on a server within the EU. Please ensure that you comply with the copyright laws of you country."))
.build();
dialog.add_response("continue", &gettext("Continue"));
@ -119,7 +119,7 @@ impl EmptyPage {
.library
.get()
.unwrap()
.import_library_from_url(&url)
.import_library_from_url(&url, Source::Metadata)
{
Ok(receiver) => {
let process = Process::new(&gettext("Downloading music library"), receiver);

View file

@ -10,7 +10,13 @@ use chrono::prelude::*;
use diesel::{prelude::*, QueryDsl, SqliteConnection};
use super::Library;
use crate::db::{self, models::*, schema::*, tables, TranslatedString};
use crate::db::{
self,
models::*,
schema::*,
tables::{self, Source},
TranslatedString,
};
impl Library {
pub fn create_person(&self, name: TranslatedString, enable_updates: bool) -> Result<Person> {
@ -21,6 +27,7 @@ impl Library {
let person = Person {
person_id: db::generate_id(),
name,
source: Source::User,
created_at: now,
edited_at: now,
last_used_at: now,
@ -86,6 +93,7 @@ impl Library {
let instrument = Instrument {
instrument_id: db::generate_id(),
name,
source: Source::User,
created_at: now,
edited_at: now,
last_used_at: now,
@ -147,6 +155,7 @@ impl Library {
let role = Role {
role_id: db::generate_id(),
name,
source: Source::User,
created_at: now,
edited_at: now,
last_used_at: now,
@ -209,7 +218,7 @@ impl Library {
) -> Result<Work> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
let work = self.create_work_priv(
let work = Self::create_work_priv(
connection,
name,
parts,
@ -226,7 +235,6 @@ impl Library {
}
fn create_work_priv(
&self,
connection: &mut SqliteConnection,
name: TranslatedString,
parts: Vec<Work>,
@ -242,8 +250,9 @@ impl Library {
let work_data = tables::Work {
work_id: work_id.clone(),
parent_work_id: parent_work_id.map(|w| w.to_string()),
sequence_number: sequence_number,
sequence_number,
name,
source: Source::User,
created_at: now,
edited_at: now,
last_used_at: now,
@ -256,7 +265,7 @@ impl Library {
.execute(connection)?;
for (index, part) in parts.into_iter().enumerate() {
self.create_work_priv(
Self::create_work_priv(
connection,
part.name,
part.parts,
@ -309,7 +318,7 @@ impl Library {
) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
self.update_work_priv(
Self::update_work_priv(
connection,
work_id,
name,
@ -327,7 +336,6 @@ impl Library {
}
fn update_work_priv(
&self,
connection: &mut SqliteConnection,
work_id: &str,
name: TranslatedString,
@ -367,7 +375,7 @@ impl Library {
.optional()?
.is_some()
{
self.update_work_priv(
Self::update_work_priv(
connection,
&part.work_id,
part.name,
@ -381,7 +389,7 @@ impl Library {
} else {
// Note: The previously used ID is discarded. This should be OK, because
// at this point, the part ID should not have been used anywhere.
self.create_work_priv(
Self::create_work_priv(
connection,
part.name,
part.parts,
@ -454,6 +462,7 @@ impl Library {
let ensemble_data = tables::Ensemble {
ensemble_id: db::generate_id(),
name,
source: Source::User,
created_at: now,
edited_at: now,
last_used_at: now,
@ -530,6 +539,7 @@ impl Library {
recording_id: recording_id.clone(),
work_id: work.work_id.clone(),
year,
source: Source::User,
created_at: now,
edited_at: now,
last_used_at: now,
@ -693,6 +703,7 @@ impl Library {
&self,
name: TranslatedString,
recordings: Vec<Recording>,
enable_updates: bool,
) -> Result<Album> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
@ -702,6 +713,8 @@ impl Library {
let album_data = tables::Album {
album_id: album_id.clone(),
name,
source: Source::User,
enable_updates,
created_at: now,
edited_at: now,
last_used_at: now,
@ -736,6 +749,7 @@ impl Library {
album_id: &str,
name: TranslatedString,
recordings: Vec<Recording>,
enable_updates: bool,
) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
@ -745,6 +759,7 @@ impl Library {
.filter(albums::album_id.eq(album_id))
.set((
albums::name.eq(name),
albums::enable_updates.eq(enable_updates),
albums::edited_at.eq(now),
albums::last_used_at.eq(now),
))

View file

@ -19,7 +19,11 @@ use zip::{write::SimpleFileOptions, ZipWriter};
use super::Library;
use crate::{
db::{self, schema::*, tables},
db::{
self,
schema::*,
tables::{self, Source},
},
process::ProcessMsg,
};
@ -28,17 +32,27 @@ impl Library {
pub fn import_library_from_zip(
&self,
path: impl AsRef<Path>,
source: Source,
) -> Result<async_channel::Receiver<ProcessMsg>> {
log::info!("Importing library from ZIP at {}", path.as_ref().to_string_lossy());
log::info!(
"Importing library from ZIP at {}",
path.as_ref().to_string_lossy()
);
let path = path.as_ref().to_owned();
let library_folder = PathBuf::from(&self.folder());
let this_connection = self.imp().connection.get().unwrap().clone();
let (sender, receiver) = async_channel::unbounded::<ProcessMsg>();
thread::spawn(move || {
if let Err(err) = sender.send_blocking(ProcessMsg::Result(
import_library_from_zip_priv(path, library_folder, this_connection, &sender),
)) {
if let Err(err) =
sender.send_blocking(ProcessMsg::Result(import_library_from_zip_priv(
path,
library_folder,
source,
this_connection,
&sender,
)))
{
log::error!("Failed to send library action result: {err:?}");
}
});
@ -52,7 +66,10 @@ impl Library {
&self,
path: impl AsRef<Path>,
) -> Result<async_channel::Receiver<ProcessMsg>> {
log::info!("Exporting library to ZIP at {}", path.as_ref().to_string_lossy());
log::info!(
"Exporting library to ZIP at {}",
path.as_ref().to_string_lossy()
);
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
let path = path.as_ref().to_owned();
@ -78,6 +95,7 @@ impl Library {
pub fn import_library_from_url(
&self,
url: &str,
source: Source,
) -> Result<async_channel::Receiver<ProcessMsg>> {
log::info!("Importing library from URL {url}");
let url = url.to_owned();
@ -88,7 +106,7 @@ impl Library {
thread::spawn(move || {
if let Err(err) = sender.send_blocking(ProcessMsg::Result(
import_library_from_url_priv(url, library_folder, this_connection, &sender),
import_library_from_url_priv(url, library_folder, source, this_connection, &sender),
)) {
log::error!("Failed to send library action result: {err:?}");
}
@ -101,6 +119,7 @@ impl Library {
pub fn import_metadata_from_url(
&self,
url: &str,
source: Source,
) -> Result<async_channel::Receiver<ProcessMsg>> {
log::info!("Importing metadata from URL {url}");
@ -111,7 +130,7 @@ impl Library {
thread::spawn(move || {
if let Err(err) = sender.send_blocking(ProcessMsg::Result(
import_metadata_from_url_priv(url, this_connection, &sender),
import_metadata_from_url_priv(url, source, this_connection, &sender),
)) {
log::error!("Failed to send library action result: {err:?}");
}
@ -125,6 +144,7 @@ impl Library {
fn import_library_from_zip_priv(
zip_path: impl AsRef<Path>,
library_folder: impl AsRef<Path>,
source: Source,
this_connection: Arc<Mutex<SqliteConnection>>,
sender: &async_channel::Sender<ProcessMsg>,
) -> Result<()> {
@ -138,7 +158,7 @@ fn import_library_from_zip_priv(
)?;
// Import metadata.
let tracks = import_metadata_from_file(tmp_db_file.path(), this_connection, false)?;
let tracks = import_metadata_from_file(tmp_db_file.path(), source, this_connection, false)?;
// Import audio files.
let n_tracks = tracks.len();
@ -212,6 +232,7 @@ fn add_file_to_zip(
fn import_metadata_from_url_priv(
url: String,
source: Source,
this_connection: Arc<Mutex<SqliteConnection>>,
sender: &async_channel::Sender<ProcessMsg>,
) -> Result<()> {
@ -223,20 +244,18 @@ fn import_metadata_from_url_priv(
formatx!(gettext("Downloading {}"), &url).unwrap(),
));
match runtime.block_on(download_tmp_file(&url, &sender)) {
match runtime.block_on(download_tmp_file(&url, sender)) {
Ok(db_file) => {
let _ = sender.send_blocking(ProcessMsg::Message(
formatx!(gettext("Importing downloaded library"), &url).unwrap(),
));
let _ = sender.send_blocking(ProcessMsg::Result(
import_metadata_from_file(db_file.path(), this_connection, true).and_then(
import_metadata_from_file(db_file.path(), source, this_connection, true).map(
|tracks| {
if !tracks.is_empty() {
log::warn!("The metadata file at {url} contains tracks.");
}
Ok(())
},
),
));
@ -252,6 +271,7 @@ fn import_metadata_from_url_priv(
fn import_library_from_url_priv(
url: String,
library_folder: impl AsRef<Path>,
source: Source,
this_connection: Arc<Mutex<SqliteConnection>>,
sender: &async_channel::Sender<ProcessMsg>,
) -> Result<()> {
@ -263,7 +283,7 @@ fn import_library_from_url_priv(
formatx!(gettext("Downloading {}"), &url).unwrap(),
));
let archive_file = runtime.block_on(download_tmp_file(&url, &sender));
let archive_file = runtime.block_on(download_tmp_file(&url, sender));
match archive_file {
Ok(archive_file) => {
@ -274,8 +294,9 @@ fn import_library_from_url_priv(
let _ = sender.send_blocking(ProcessMsg::Result(import_library_from_zip_priv(
archive_file.path(),
library_folder,
source,
this_connection,
&sender,
sender,
)));
}
Err(err) => {
@ -293,6 +314,7 @@ fn import_library_from_url_priv(
/// In any case, tracks are returned.
fn import_metadata_from_file(
path: impl AsRef<Path>,
source: Source,
this_connection: Arc<Mutex<SqliteConnection>>,
ignore_tracks: bool,
) -> Result<Vec<tables::Track>> {
@ -327,6 +349,7 @@ fn import_metadata_from_file(
// Import metadata that is not already present.
for mut person in persons {
person.source = source;
person.created_at = now;
person.edited_at = now;
person.last_used_at = now;
@ -339,6 +362,7 @@ fn import_metadata_from_file(
}
for mut role in roles {
role.source = source;
role.created_at = now;
role.edited_at = now;
role.last_used_at = now;
@ -350,6 +374,7 @@ fn import_metadata_from_file(
}
for mut instrument in instruments {
instrument.source = source;
instrument.created_at = now;
instrument.edited_at = now;
instrument.last_used_at = now;
@ -362,6 +387,7 @@ fn import_metadata_from_file(
}
for mut work in works {
work.source = source;
work.created_at = now;
work.edited_at = now;
work.last_used_at = now;
@ -388,6 +414,7 @@ fn import_metadata_from_file(
}
for mut ensemble in ensembles {
ensemble.source = source;
ensemble.created_at = now;
ensemble.edited_at = now;
ensemble.last_used_at = now;
@ -407,6 +434,7 @@ fn import_metadata_from_file(
}
for mut recording in recordings {
recording.source = source;
recording.created_at = now;
recording.edited_at = now;
recording.last_used_at = now;
@ -466,6 +494,7 @@ fn import_metadata_from_file(
}
for mut album in albums {
album.source = source;
album.created_at = now;
album.edited_at = now;
album.last_used_at = now;

View file

@ -414,7 +414,6 @@ impl Library {
works,
recordings,
albums,
..Default::default()
}
}
LibraryQuery {

View file

@ -9,8 +9,8 @@ use gtk::{
};
use crate::{
config, library::Library, process::Process, process_manager::ProcessManager,
process_row::ProcessRow, window::Window,
config, db::tables::Source, library::Library, process::Process,
process_manager::ProcessManager, process_row::ProcessRow, window::Window,
};
mod imp {
@ -128,7 +128,13 @@ impl LibraryManager {
}
Ok(path) => {
if let Some(path) = path.path() {
match self.imp().library.get().unwrap().import_library_from_zip(&path) {
match self
.imp()
.library
.get()
.unwrap()
.import_library_from_zip(&path, Source::Import)
{
Ok(receiver) => {
let process = Process::new(
&formatx!(
@ -186,7 +192,13 @@ impl LibraryManager {
}
Ok(path) => {
if let Some(path) = path.path() {
match self.imp().library.get().unwrap().export_library_to_zip(&path) {
match self
.imp()
.library
.get()
.unwrap()
.export_library_to_zip(&path)
{
Ok(receiver) => {
let process = Process::new(
&formatx!(
@ -228,7 +240,7 @@ impl LibraryManager {
.library
.get()
.unwrap()
.import_metadata_from_url(&url)
.import_metadata_from_url(&url, Source::Metadata)
{
Ok(receiver) => {
let process = Process::new(&gettext("Updating metadata"), receiver);
@ -259,7 +271,7 @@ impl LibraryManager {
.library
.get()
.unwrap()
.import_library_from_url(&url)
.import_library_from_url(&url, Source::Metadata)
{
Ok(receiver) => {
let process = Process::new(&gettext("Updating music library"), receiver);

View file

@ -20,7 +20,6 @@ mod program;
mod program_tile;
mod recording_tile;
mod search_page;
mod search_tag;
mod selector;
mod slider_row;
mod tag_tile;
@ -47,7 +46,7 @@ fn main() -> glib::ExitCode {
gettextrs::textdomain(config::PKGNAME).unwrap();
gio::resources_register(
&gio::Resource::load(&format!(
&gio::Resource::load(format!(
"{}/{}/{}.gresource",
config::DATADIR,
config::PKGNAME,

View file

@ -221,14 +221,14 @@ impl Player {
items.push(PlaylistItem::new(
true,
recording.work.composers_string(),
&recording.work.name.get(),
recording.work.name.get(),
Some(&performances),
None,
&self.library_path_to_file_path(&tracks[0].path),
self.library_path_to_file_path(&tracks[0].path),
&tracks[0].track_id,
));
} else {
let mut tracks = tracks.into_iter();
let mut tracks = tracks.iter();
let first_track = tracks.next().unwrap();
let track_title = |track: &Track, number: usize| -> String {
@ -249,10 +249,10 @@ impl Player {
items.push(PlaylistItem::new(
true,
recording.work.composers_string(),
&recording.work.name.get(),
recording.work.name.get(),
Some(&performances),
Some(&track_title(&first_track, 1)),
&self.library_path_to_file_path(&first_track.path),
Some(&track_title(first_track, 1)),
self.library_path_to_file_path(&first_track.path),
&first_track.track_id,
));
@ -260,11 +260,11 @@ impl Player {
items.push(PlaylistItem::new(
false,
recording.work.composers_string(),
&recording.work.name.get(),
recording.work.name.get(),
Some(&performances),
// track number = track index + 1 (first track) + 1 (zero based)
Some(&track_title(&track, index + 2)),
&self.library_path_to_file_path(&track.path),
Some(&track_title(track, index + 2)),
self.library_path_to_file_path(&track.path),
&track.track_id,
));
}

View file

@ -83,7 +83,7 @@ impl Program {
}
pub fn from_query(query: LibraryQuery) -> Self {
let settings = gio::Settings::new(&config::APP_ID);
let settings = gio::Settings::new(config::APP_ID);
glib::Object::builder()
.property(

View file

@ -56,7 +56,7 @@ mod imp {
self.set_program_from_settings(&settings);
let obj = self.obj().to_owned();
settings.connect_changed(Some(&self.key.get().unwrap()), move |settings, _| {
settings.connect_changed(Some(self.key.get().unwrap()), move |settings, _| {
obj.imp().set_program_from_settings(settings);
});
}

View file

@ -69,7 +69,7 @@ mod imp {
.push(&RecordingEditor::new(
obj.imp().navigation.get().unwrap(),
obj.imp().library.get().unwrap(),
Some(&obj.imp().recording.get().unwrap()),
Some(obj.imp().recording.get().unwrap()),
));
})
.build();
@ -90,8 +90,8 @@ mod imp {
let delete_action = gio::ActionEntry::builder("delete")
.activate(move |_, _, _| {
let dialog = adw::AlertDialog::builder()
.heading(&gettext("Delete recording?"))
.body(&gettext("The recording will be removed from your music library and the corresponding audio files will be deleted. This action cannot be undone."))
.heading(gettext("Delete recording?"))
.body(gettext("The recording will be removed from your music library and the corresponding audio files will be deleted. This action cannot be undone."))
.build();
dialog.add_response("delete", &gettext("Delete"));
@ -142,7 +142,7 @@ impl RecordingTile {
let obj: Self = glib::Object::new();
let imp = obj.imp();
imp.work_label.set_label(&recording.work.name.get());
imp.work_label.set_label(recording.work.name.get());
imp.composer_label.set_label(
&recording
.work

View file

@ -22,8 +22,7 @@ use crate::{
program::Program,
program_tile::ProgramTile,
recording_tile::RecordingTile,
search_tag::Tag,
tag_tile::TagTile,
tag_tile::{Tag, TagTile},
util,
};
@ -392,7 +391,7 @@ impl SearchPage {
imp.header_box.set_visible(!query.is_empty());
let highlight = if let Some(work) = &query.work {
imp.title_label.set_text(&work.name.get());
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);
@ -401,15 +400,15 @@ impl SearchPage {
}
Some(Tag::Work(work.to_owned()))
} else if let Some(person) = &query.composer {
imp.title_label.set_text(&person.name.get());
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.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.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 {

View file

@ -1,98 +0,0 @@
use std::cell::OnceCell;
use adw::{glib, glib::subclass::Signal, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy;
use crate::db::models::{Ensemble, Instrument, Person, Work};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/search_tag.blp")]
pub struct SearchTag {
#[template_child]
pub label: TemplateChild<gtk::Label>,
pub tag: OnceCell<Tag>,
}
#[glib::object_subclass]
impl ObjectSubclass for SearchTag {
const NAME: &'static str = "MusicusSearchTag";
type Type = super::SearchTag;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_instance_callbacks();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for SearchTag {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
SIGNALS.as_ref()
}
}
impl WidgetImpl for SearchTag {}
impl BoxImpl for SearchTag {}
}
glib::wrapper! {
pub struct SearchTag(ObjectSubclass<imp::SearchTag>)
@extends gtk::Widget;
}
#[gtk::template_callbacks]
impl SearchTag {
pub fn new(tag: Tag) -> Self {
let obj: SearchTag = glib::Object::new();
let label = match &tag {
Tag::Composer(person) => person.name.get(),
Tag::Performer(person) => person.name.get(),
Tag::Ensemble(ensemble) => ensemble.name.get(),
Tag::Instrument(instrument) => instrument.name.get(),
Tag::Work(work) => work.name.get(),
};
obj.imp().label.set_label(label);
obj.set_tooltip_text(Some(label));
obj.imp().tag.set(tag).unwrap();
obj
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
pub fn tag(&self) -> &Tag {
self.imp().tag.get().unwrap()
}
#[template_callback]
fn remove(&self) {
self.emit_by_name::<()>("remove", &[]);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Tag {
Composer(Person),
Performer(Person),
Ensemble(Ensemble),
Instrument(Instrument),
Work(Work),
}

View file

@ -294,7 +294,7 @@ impl RecordingSelectorPopover {
.build(),
);
row.set_tooltip_text(Some(&work.name.get()));
row.set_tooltip_text(Some(work.name.get()));
let work = work.clone();
let obj = self.clone();

View file

@ -256,7 +256,7 @@ impl WorkSelectorPopover {
.build(),
);
row.set_tooltip_text(Some(&work.name.get()));
row.set_tooltip_text(Some(work.name.get()));
let work = work.clone();
let obj = self.clone();

View file

@ -2,7 +2,7 @@ use std::cell::OnceCell;
use gtk::{glib, prelude::*, subclass::prelude::*};
use crate::search_tag::Tag;
use crate::db::models::{Ensemble, Instrument, Person, Work};
mod imp {
use super::*;
@ -78,3 +78,12 @@ impl TagTile {
self.imp().tag.get().unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Tag {
Composer(Person),
Performer(Person),
Ensemble(Ensemble),
Instrument(Instrument),
Work(Work),
}

View file

@ -2,34 +2,33 @@ pub mod activatable_row;
pub mod drag_widget;
pub mod error_dialog;
use std::sync::LazyLock;
use gettextrs::gettext;
use gtk::glib::{self, clone};
use lazy_static::lazy_static;
use error_dialog::ErrorDialog;
lazy_static! {
/// The user's language code.
pub static ref LANG: String = {
let lang = match glib::language_names().first() {
Some(language_name) => match language_name.split('_').next() {
Some(lang) => lang.to_string(),
None => "generic".to_string(),
},
/// The user's language code.
pub static LANG: LazyLock<String> = LazyLock::new(|| {
let lang = match glib::language_names().first() {
Some(language_name) => match language_name.split('_').next() {
Some(lang) => lang.to_string(),
None => "generic".to_string(),
};
log::info!("Intialized user language to '{lang}'.");
lang
},
None => "generic".to_string(),
};
}
log::info!("Intialized user language to '{lang}'.");
lang
});
/// Create and show an error toast. This will also log the error to the console.
pub fn error_toast(msgid: &str, err: anyhow::Error, toast_overlay: &adw::ToastOverlay) {
log::error!("{msgid}: {err:?}");
let toast = adw::Toast::builder()
.title(&gettext(msgid))
.title(gettext(msgid))
.button_label("Details")
.build();

View file

@ -61,7 +61,7 @@ glib::wrapper! {
impl ErrorDialog {
pub fn present(err: &anyhow::Error, parent: &impl IsA<gtk::Widget>) {
let obj: Self = glib::Object::builder()
.property("error-text", &format!("{err:?}"))
.property("error-text", format!("{err:?}"))
.build();
obj.present(Some(parent));

View file

@ -1,4 +1,7 @@
use std::{cell::RefCell, path::Path};
use std::{
cell::{Cell, RefCell},
path::Path,
};
use adw::{prelude::*, subclass::prelude::*};
use anyhow::{anyhow, Result};
@ -6,8 +9,10 @@ use gettextrs::gettext;
use gtk::{gio, glib, glib::clone};
use crate::{
album_page::AlbumPage,
config,
editor::tracks::TracksEditor,
db::tables::Source,
editor::{album::AlbumEditor, tracks::TracksEditor},
empty_page::EmptyPage,
library::{Library, LibraryQuery},
library_manager::LibraryManager,
@ -31,6 +36,7 @@ mod imp {
pub library: RefCell<Option<Library>>,
pub player: Player,
pub process_manager: ProcessManager,
pub inhibitor_cookie: Cell<Option<u32>>,
#[template_child]
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
@ -83,6 +89,16 @@ mod imp {
})
.build();
let obj = self.obj().to_owned();
let create_album_action = gio::ActionEntry::builder("create-album")
.activate(move |_, _, _| {
if let Some(library) = &*obj.imp().library.borrow() {
let editor = AlbumEditor::new(&obj.imp().navigation_view, library, None);
obj.imp().navigation_view.push(&editor);
}
})
.build();
let obj = self.obj().to_owned();
let library_action = gio::ActionEntry::builder("library")
.activate(move |_, _, _| {
@ -104,8 +120,12 @@ mod imp {
})
.build();
self.obj()
.add_action_entries([import_action, library_action, preferences_action]);
self.obj().add_action_entries([
import_action,
create_album_action,
library_action,
preferences_action,
]);
let player_bar = PlayerBar::new(&self.player);
self.player_bar_revealer.set_child(Some(&player_bar));
@ -148,6 +168,25 @@ mod imp {
let obj = self.obj().to_owned();
self.player.connect_raise(move |_| obj.present());
let obj = self.obj().to_owned();
self.player.connect_playing_notify(move |player| {
if let Some(app) = obj.application() {
if let Some(cookie) = obj.imp().inhibitor_cookie.take() {
app.uninhibit(cookie);
};
if player.playing() {
let cookie = app.inhibit(
Some(&obj),
gtk::ApplicationInhibitFlags::SUSPEND,
Some(&gettext("Currently playing music")),
);
obj.imp().inhibitor_cookie.set(Some(cookie));
}
}
});
let settings = gio::Settings::new(config::APP_ID);
let library_path = settings.string("library-path").to_string();
if !library_path.is_empty() {
@ -164,8 +203,8 @@ mod imp {
fn close_request(&self) -> glib::signal::Propagation {
if self.process_manager.any_ongoing() {
let dialog = adw::AlertDialog::builder()
.heading(&gettext("Close window?"))
.body(&gettext(
.heading(gettext("Close window?"))
.body(gettext(
"There are ongoing processes that will be canceled.",
))
.build();
@ -277,7 +316,7 @@ impl Window {
config::METADATA_URL.to_string()
};
match library.import_metadata_from_url(&url) {
match library.import_metadata_from_url(&url, Source::Metadata) {
Ok(receiver) => {
let process = Process::new(&gettext("Updating metadata"), receiver);
self.imp().process_manager.add_process(&process);
@ -339,17 +378,12 @@ impl Window {
fn reset_view(&self) {
let navigation = self.imp().navigation_view.get();
// Get all pages that are not instances of SearchPage.
// Get all pages that are not instances of SearchPage or AlbumPage.
let mut navigation_stack = navigation
.navigation_stack()
.iter::<adw::NavigationPage>()
.filter_map(|page| match page {
Ok(page) => match page.downcast_ref::<SearchPage>() {
Some(_) => None,
None => Some(page),
},
Err(_) => None,
})
.filter_map(|page| page.ok())
.filter(|page| !page.is::<SearchPage>() && !page.is::<AlbumPage>())
.collect::<Vec<adw::NavigationPage>>();
navigation_stack.insert(