Compare commits

..

4 commits

29 changed files with 161 additions and 251 deletions

View file

@ -14,17 +14,6 @@
padding-bottom: 3px; 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 { .tile {
min-height: 50px; min-height: 50px;
min-width: 200px; min-width: 200px;

View file

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

View file

@ -80,7 +80,7 @@ template $MusicusSearchPage: Adw.NavigationPage {
activate => $select() swapped; activate => $select() swapped;
styles [ styles [
"rounded-entry" "rounded-entry",
] ]
} }
@ -264,16 +264,24 @@ template $MusicusSearchPage: Adw.NavigationPage {
} }
menu primary_menu { menu primary_menu {
section {
item { item {
label: _("_Import music"); label: _("_Import music");
action: "win.import"; action: "win.import";
} }
item {
label: _("_Create album");
action: "win.create-album";
}
item { item {
label: _("_Library manager"); label: _("_Library manager");
action: "win.library"; action: "win.library";
} }
}
section {
item { item {
label: _("_Preferences"); label: _("_Preferences");
action: "win.preferences"; action: "win.preferences";
@ -284,6 +292,7 @@ menu primary_menu {
action: "app.about"; action: "app.about";
} }
} }
}
menu item_menu { menu item_menu {
section { 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() .unwrap()
.recordings .recordings
.iter() .iter()
.map(|r| obj.player().recording_to_playlist(r)) .flat_map(|r| obj.player().recording_to_playlist(r))
.flatten()
.collect::<Vec<PlaylistItem>>(); .collect::<Vec<PlaylistItem>>();
if let Err(err) = obj.player().append(playlist) { if let Err(err) = obj.player().append(playlist) {
@ -165,8 +164,7 @@ impl AlbumPage {
.unwrap() .unwrap()
.recordings .recordings
.iter() .iter()
.map(|r| self.player().recording_to_playlist(r)) .flat_map(|r| self.player().recording_to_playlist(r))
.flatten()
.collect::<Vec<PlaylistItem>>(); .collect::<Vec<PlaylistItem>>();
self.player().append_and_play(playlist); self.player().append_and_play(playlist);

View file

@ -45,7 +45,7 @@ impl AlbumTile {
pub fn new(album: &Album) -> Self { pub fn new(album: &Album) -> Self {
let obj: Self = glib::Object::new(); 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.imp().album.set(album.clone()).unwrap();
obj obj

View file

@ -69,7 +69,7 @@ mod imp {
self.parent_constructed(); self.parent_constructed();
let set_design_action = gio::ActionEntry::builder("set-design") 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")) .state(glib::Variant::from("program-1"))
.build(); .build();

View file

@ -246,7 +246,7 @@ impl RecordingEditor {
} }
fn set_work(&self, work: Work) { 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( self.imp().work_row.set_subtitle(
&work &work
.composers_string() .composers_string()

View file

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

View file

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

View file

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

View file

@ -92,8 +92,8 @@ impl EmptyPage {
#[template_callback] #[template_callback]
async fn download_library(&self) { async fn download_library(&self) {
let dialog = adw::AlertDialog::builder() let dialog = adw::AlertDialog::builder()
.heading(&gettext("Disclaimer")) .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.")) .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(); .build();
dialog.add_response("continue", &gettext("Continue")); dialog.add_response("continue", &gettext("Continue"));

View file

@ -209,7 +209,7 @@ impl Library {
) -> Result<Work> { ) -> Result<Work> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap(); let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
let work = self.create_work_priv( let work = Self::create_work_priv(
connection, connection,
name, name,
parts, parts,
@ -226,7 +226,6 @@ impl Library {
} }
fn create_work_priv( fn create_work_priv(
&self,
connection: &mut SqliteConnection, connection: &mut SqliteConnection,
name: TranslatedString, name: TranslatedString,
parts: Vec<Work>, parts: Vec<Work>,
@ -242,7 +241,7 @@ impl Library {
let work_data = tables::Work { let work_data = tables::Work {
work_id: work_id.clone(), work_id: work_id.clone(),
parent_work_id: parent_work_id.map(|w| w.to_string()), parent_work_id: parent_work_id.map(|w| w.to_string()),
sequence_number: sequence_number, sequence_number,
name, name,
created_at: now, created_at: now,
edited_at: now, edited_at: now,
@ -256,7 +255,7 @@ impl Library {
.execute(connection)?; .execute(connection)?;
for (index, part) in parts.into_iter().enumerate() { for (index, part) in parts.into_iter().enumerate() {
self.create_work_priv( Self::create_work_priv(
connection, connection,
part.name, part.name,
part.parts, part.parts,
@ -309,7 +308,7 @@ impl Library {
) -> Result<()> { ) -> Result<()> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap(); let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
self.update_work_priv( Self::update_work_priv(
connection, connection,
work_id, work_id,
name, name,
@ -327,7 +326,6 @@ impl Library {
} }
fn update_work_priv( fn update_work_priv(
&self,
connection: &mut SqliteConnection, connection: &mut SqliteConnection,
work_id: &str, work_id: &str,
name: TranslatedString, name: TranslatedString,
@ -367,7 +365,7 @@ impl Library {
.optional()? .optional()?
.is_some() .is_some()
{ {
self.update_work_priv( Self::update_work_priv(
connection, connection,
&part.work_id, &part.work_id,
part.name, part.name,
@ -381,7 +379,7 @@ impl Library {
} else { } else {
// Note: The previously used ID is discarded. This should be OK, because // Note: The previously used ID is discarded. This should be OK, because
// at this point, the part ID should not have been used anywhere. // at this point, the part ID should not have been used anywhere.
self.create_work_priv( Self::create_work_priv(
connection, connection,
part.name, part.name,
part.parts, part.parts,

View file

@ -29,7 +29,10 @@ impl Library {
&self, &self,
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> Result<async_channel::Receiver<ProcessMsg>> { ) -> 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 path = path.as_ref().to_owned();
let library_folder = PathBuf::from(&self.folder()); let library_folder = PathBuf::from(&self.folder());
let this_connection = self.imp().connection.get().unwrap().clone(); let this_connection = self.imp().connection.get().unwrap().clone();
@ -52,7 +55,10 @@ impl Library {
&self, &self,
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> Result<async_channel::Receiver<ProcessMsg>> { ) -> 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 connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
let path = path.as_ref().to_owned(); let path = path.as_ref().to_owned();
@ -223,22 +229,18 @@ fn import_metadata_from_url_priv(
formatx!(gettext("Downloading {}"), &url).unwrap(), 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) => { Ok(db_file) => {
let _ = sender.send_blocking(ProcessMsg::Message( let _ = sender.send_blocking(ProcessMsg::Message(
formatx!(gettext("Importing downloaded library"), &url).unwrap(), formatx!(gettext("Importing downloaded library"), &url).unwrap(),
)); ));
let _ = sender.send_blocking(ProcessMsg::Result( 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(), this_connection, true).map(|tracks| {
|tracks| {
if !tracks.is_empty() { if !tracks.is_empty() {
log::warn!("The metadata file at {url} contains tracks."); log::warn!("The metadata file at {url} contains tracks.");
} }
}),
Ok(())
},
),
)); ));
} }
Err(err) => { Err(err) => {
@ -263,7 +265,7 @@ fn import_library_from_url_priv(
formatx!(gettext("Downloading {}"), &url).unwrap(), 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 { match archive_file {
Ok(archive_file) => { Ok(archive_file) => {
@ -275,7 +277,7 @@ fn import_library_from_url_priv(
archive_file.path(), archive_file.path(),
library_folder, library_folder,
this_connection, this_connection,
&sender, sender,
))); )));
} }
Err(err) => { Err(err) => {

View file

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

View file

@ -128,7 +128,13 @@ impl LibraryManager {
} }
Ok(path) => { Ok(path) => {
if let Some(path) = path.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) => { Ok(receiver) => {
let process = Process::new( let process = Process::new(
&formatx!( &formatx!(
@ -186,7 +192,13 @@ impl LibraryManager {
} }
Ok(path) => { Ok(path) => {
if let Some(path) = path.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) => { Ok(receiver) => {
let process = Process::new( let process = Process::new(
&formatx!( &formatx!(

View file

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

View file

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

View file

@ -83,7 +83,7 @@ impl Program {
} }
pub fn from_query(query: LibraryQuery) -> Self { 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() glib::Object::builder()
.property( .property(

View file

@ -56,7 +56,7 @@ mod imp {
self.set_program_from_settings(&settings); self.set_program_from_settings(&settings);
let obj = self.obj().to_owned(); 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); obj.imp().set_program_from_settings(settings);
}); });
} }

View file

@ -69,7 +69,7 @@ mod imp {
.push(&RecordingEditor::new( .push(&RecordingEditor::new(
obj.imp().navigation.get().unwrap(), obj.imp().navigation.get().unwrap(),
obj.imp().library.get().unwrap(), obj.imp().library.get().unwrap(),
Some(&obj.imp().recording.get().unwrap()), Some(obj.imp().recording.get().unwrap()),
)); ));
}) })
.build(); .build();
@ -90,8 +90,8 @@ mod imp {
let delete_action = gio::ActionEntry::builder("delete") let delete_action = gio::ActionEntry::builder("delete")
.activate(move |_, _, _| { .activate(move |_, _, _| {
let dialog = adw::AlertDialog::builder() let dialog = adw::AlertDialog::builder()
.heading(&gettext("Delete recording?")) .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.")) .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(); .build();
dialog.add_response("delete", &gettext("Delete")); dialog.add_response("delete", &gettext("Delete"));
@ -142,7 +142,7 @@ impl RecordingTile {
let obj: Self = glib::Object::new(); let obj: Self = glib::Object::new();
let imp = obj.imp(); 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( imp.composer_label.set_label(
&recording &recording
.work .work

View file

@ -22,8 +22,7 @@ use crate::{
program::Program, program::Program,
program_tile::ProgramTile, program_tile::ProgramTile,
recording_tile::RecordingTile, recording_tile::RecordingTile,
search_tag::Tag, tag_tile::{Tag, TagTile},
tag_tile::TagTile,
util, util,
}; };
@ -392,7 +391,7 @@ impl SearchPage {
imp.header_box.set_visible(!query.is_empty()); imp.header_box.set_visible(!query.is_empty());
let highlight = if let Some(work) = &query.work { 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() { if let Some(composers) = work.composers_string() {
imp.subtitle_label.set_text(&composers); imp.subtitle_label.set_text(&composers);
imp.subtitle_label.set_visible(true); imp.subtitle_label.set_visible(true);
@ -401,15 +400,15 @@ impl SearchPage {
} }
Some(Tag::Work(work.to_owned())) Some(Tag::Work(work.to_owned()))
} else if let Some(person) = &query.composer { } 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); imp.subtitle_label.set_visible(false);
Some(Tag::Composer(person.to_owned())) Some(Tag::Composer(person.to_owned()))
} else if let Some(person) = &query.performer { } 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); imp.subtitle_label.set_visible(false);
Some(Tag::Performer(person.to_owned())) Some(Tag::Performer(person.to_owned()))
} else if let Some(ensemble) = &query.ensemble { } 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); imp.subtitle_label.set_visible(false);
Some(Tag::Ensemble(ensemble.to_owned())) Some(Tag::Ensemble(ensemble.to_owned()))
} else if let Some(instrument) = &query.instrument { } 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(), .build(),
); );
row.set_tooltip_text(Some(&work.name.get())); row.set_tooltip_text(Some(work.name.get()));
let work = work.clone(); let work = work.clone();
let obj = self.clone(); let obj = self.clone();

View file

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

View file

@ -2,7 +2,7 @@ use std::cell::OnceCell;
use gtk::{glib, prelude::*, subclass::prelude::*}; use gtk::{glib, prelude::*, subclass::prelude::*};
use crate::search_tag::Tag; use crate::db::models::{Ensemble, Instrument, Person, Work};
mod imp { mod imp {
use super::*; use super::*;
@ -78,3 +78,12 @@ impl TagTile {
self.imp().tag.get().unwrap() 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:?}"); log::error!("{msgid}: {err:?}");
let toast = adw::Toast::builder() let toast = adw::Toast::builder()
.title(&gettext(msgid)) .title(gettext(msgid))
.button_label("Details") .button_label("Details")
.build(); .build();

View file

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

View file

@ -9,8 +9,9 @@ use gettextrs::gettext;
use gtk::{gio, glib, glib::clone}; use gtk::{gio, glib, glib::clone};
use crate::{ use crate::{
album_page::AlbumPage,
config, config,
editor::tracks::TracksEditor, editor::{album::AlbumEditor, tracks::TracksEditor},
empty_page::EmptyPage, empty_page::EmptyPage,
library::{Library, LibraryQuery}, library::{Library, LibraryQuery},
library_manager::LibraryManager, library_manager::LibraryManager,
@ -87,6 +88,16 @@ mod imp {
}) })
.build(); .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 obj = self.obj().to_owned();
let library_action = gio::ActionEntry::builder("library") let library_action = gio::ActionEntry::builder("library")
.activate(move |_, _, _| { .activate(move |_, _, _| {
@ -108,8 +119,12 @@ mod imp {
}) })
.build(); .build();
self.obj() self.obj().add_action_entries([
.add_action_entries([import_action, library_action, preferences_action]); import_action,
create_album_action,
library_action,
preferences_action,
]);
let player_bar = PlayerBar::new(&self.player); let player_bar = PlayerBar::new(&self.player);
self.player_bar_revealer.set_child(Some(&player_bar)); self.player_bar_revealer.set_child(Some(&player_bar));
@ -187,8 +202,8 @@ mod imp {
fn close_request(&self) -> glib::signal::Propagation { fn close_request(&self) -> glib::signal::Propagation {
if self.process_manager.any_ongoing() { if self.process_manager.any_ongoing() {
let dialog = adw::AlertDialog::builder() let dialog = adw::AlertDialog::builder()
.heading(&gettext("Close window?")) .heading(gettext("Close window?"))
.body(&gettext( .body(gettext(
"There are ongoing processes that will be canceled.", "There are ongoing processes that will be canceled.",
)) ))
.build(); .build();
@ -362,17 +377,12 @@ impl Window {
fn reset_view(&self) { fn reset_view(&self) {
let navigation = self.imp().navigation_view.get(); 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 let mut navigation_stack = navigation
.navigation_stack() .navigation_stack()
.iter::<adw::NavigationPage>() .iter::<adw::NavigationPage>()
.filter_map(|page| match page { .filter_map(|page| page.ok())
Ok(page) => match page.downcast_ref::<SearchPage>() { .filter(|page| !page.is::<SearchPage>() && !page.is::<AlbumPage>())
Some(_) => None,
None => Some(page),
},
Err(_) => None,
})
.collect::<Vec<adw::NavigationPage>>(); .collect::<Vec<adw::NavigationPage>>();
navigation_stack.insert( navigation_stack.insert(