Compare commits

...

4 commits

29 changed files with 161 additions and 251 deletions

View file

@ -14,17 +14,6 @@
padding-bottom: 3px;
}
.searchbar .searchtag {
background-color: alpha(currentColor, 0.1);
border-radius: 100px;
}
.searchbar .searchtag>button {
min-width: 24px;
min-height: 24px;
margin: 0px;
}
.tile {
min-height: 50px;
min-width: 200px;

View file

@ -50,16 +50,24 @@ template $MusicusEmptyPage: Adw.NavigationPage {
}
menu primary_menu {
section {
item {
label: _("_Import music");
action: "win.import";
}
item {
label: _("_Create album");
action: "win.create-album";
}
item {
label: _("_Library manager");
action: "win.library";
}
}
section {
item {
label: _("_Preferences");
action: "win.preferences";
@ -70,3 +78,4 @@ menu primary_menu {
action: "app.about";
}
}
}

View file

@ -80,7 +80,7 @@ template $MusicusSearchPage: Adw.NavigationPage {
activate => $select() swapped;
styles [
"rounded-entry"
"rounded-entry",
]
}
@ -264,16 +264,24 @@ template $MusicusSearchPage: Adw.NavigationPage {
}
menu primary_menu {
section {
item {
label: _("_Import music");
action: "win.import";
}
item {
label: _("_Create album");
action: "win.create-album";
}
item {
label: _("_Library manager");
action: "win.library";
}
}
section {
item {
label: _("_Preferences");
action: "win.preferences";
@ -284,6 +292,7 @@ menu primary_menu {
action: "app.about";
}
}
}
menu item_menu {
section {

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

@ -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,8 +390,7 @@ impl WorkEditor {
};
self.emit_by_name::<()>("created", &[&part]);
} else {
if let Some(work_id) = self.imp().work_id.get() {
} else if let Some(work_id) = self.imp().work_id.get() {
library
.update_work(work_id, name, parts, composers, instruments, enable_updates)
.unwrap();
@ -401,7 +400,6 @@ impl WorkEditor {
.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| {
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.");
}
Ok(())
},
),
}),
));
}
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

@ -28,7 +28,7 @@ pub fn error_toast(msgid: &str, err: anyhow::Error, toast_overlay: &adw::ToastOv
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

@ -9,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,
@ -87,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 |_, _, _| {
@ -108,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));
@ -187,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();
@ -362,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(