Compare commits

...

11 commits
v0.1.0 ... main

35 changed files with 230 additions and 291 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

@ -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

@ -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

@ -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

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

View file

@ -209,7 +209,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 +226,6 @@ impl Library {
}
fn create_work_priv(
&self,
connection: &mut SqliteConnection,
name: TranslatedString,
parts: Vec<Work>,
@ -242,7 +241,7 @@ 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,
created_at: now,
edited_at: now,
@ -256,7 +255,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 +308,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 +326,6 @@ impl Library {
}
fn update_work_priv(
&self,
connection: &mut SqliteConnection,
work_id: &str,
name: TranslatedString,
@ -367,7 +365,7 @@ impl Library {
.optional()?
.is_some()
{
self.update_work_priv(
Self::update_work_priv(
connection,
&part.work_id,
part.name,
@ -381,7 +379,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,

View file

@ -29,7 +29,10 @@ impl Library {
&self,
path: impl AsRef<Path>,
) -> 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();
@ -52,7 +55,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();
@ -223,22 +229,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(
|tracks| {
if !tracks.is_empty() {
log::warn!("The metadata file at {url} contains tracks.");
}
Ok(())
},
),
import_metadata_from_file(db_file.path(), this_connection, true).map(|tracks| {
if !tracks.is_empty() {
log::warn!("The metadata file at {url} contains tracks.");
}
}),
));
}
Err(err) => {
@ -263,7 +265,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) => {
@ -275,7 +277,7 @@ fn import_library_from_url_priv(
archive_file.path(),
library_folder,
this_connection,
&sender,
sender,
)));
}
Err(err) => {

View file

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

View file

@ -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)
{
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!(

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,9 @@ use gettextrs::gettext;
use gtk::{gio, glib, glib::clone};
use crate::{
album_page::AlbumPage,
config,
editor::tracks::TracksEditor,
editor::{album::AlbumEditor, tracks::TracksEditor},
empty_page::EmptyPage,
library::{Library, LibraryQuery},
library_manager::LibraryManager,
@ -31,6 +35,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 +88,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 +119,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 +167,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 +202,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();
@ -339,17 +377,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(