Add empty page offering download

This commit is contained in:
Elias Projahn 2025-03-23 16:04:14 +01:00
parent bf1ffef05a
commit 424c4c57a8
10 changed files with 295 additions and 9 deletions

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="@PATH_ID@">
<file preprocess="xml-stripblanks">icons/scalable/actions/library-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/music-note-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/playlist-symbolic.svg</file>
<file compressed="true">style.css</file>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 1.5 2 h 2 c 0.277344 0 0.5 0.222656 0.5 0.5 v 12 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -2 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -12 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0"/><path d="m 5.5 4 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 10 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -10 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0"/><path d="m 8.5 3 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 11 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -11 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0"/><path d="m 10.707031 1.460938 l 0.964844 -0.261719 c 0.265625 -0.070313 0.539063 0.089843 0.613281 0.355469 l 3.363282 12.558593 c 0.070312 0.265625 -0.085938 0.539063 -0.351563 0.609375 l -0.96875 0.261719 c -0.265625 0.070313 -0.539063 -0.089844 -0.613281 -0.355469 l -3.363282 -12.554687 c -0.070312 -0.269531 0.085938 -0.542969 0.355469 -0.613281 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

72
data/ui/empty_page.blp Normal file
View file

@ -0,0 +1,72 @@
using Gtk 4.0;
using Adw 1;
template $MusicusEmptyPage: Adw.NavigationPage {
title: _("New Library");
Adw.ToolbarView {
[top]
Adw.HeaderBar header_bar {
[end]
MenuButton {
icon-name: "open-menu-symbolic";
menu-model: primary_menu;
}
}
Adw.StatusPage {
icon-name: "library-symbolic";
title: _("New Library");
description: _("You can import your recordings by selecting \"Import music\" in the main menu. Musicus also comes with a small pre-made library of recordings. You can download it using the button below.");
child: Gtk.Box {
orientation: vertical;
Gtk.Button download_button {
halign: center;
label: _("Download music");
clicked => $download_library() swapped;
styles [
"suggested-action",
"pill",
]
}
Adw.Clamp {
Gtk.ListBox process_list {
selection-mode: none;
margin-top: 12;
visible: false;
styles [
"boxed-list-separate",
]
}
}
};
}
}
}
menu primary_menu {
item {
label: _("_Import music");
action: "win.import";
}
item {
label: _("_Library manager");
action: "win.library";
}
item {
label: _("_Preferences");
action: "win.preferences";
}
item {
label: _("_About Musicus");
action: "app.about";
}
}

View file

@ -4,6 +4,7 @@ using Adw 1;
template $MusicusPreferencesDialog: Adw.PreferencesDialog {
Adw.PreferencesPage {
title: _("Playback");
icon-name: "media-playback-start-symbolic";
Adw.PreferencesGroup {
title: _("Default program");
@ -65,6 +66,7 @@ template $MusicusPreferencesDialog: Adw.PreferencesDialog {
Adw.PreferencesPage {
title: _("Library");
icon-name: "library-symbolic";
Adw.PreferencesGroup {
title: _("Library download");

View file

@ -1,7 +1,7 @@
using Gtk 4.0;
using Adw 1;
template $MusicusWelcomePage : Adw.NavigationPage {
template $MusicusWelcomePage: Adw.NavigationPage {
title: _("Welcome to Musicus");
tag: "welcome";
@ -15,12 +15,17 @@ template $MusicusWelcomePage : Adw.NavigationPage {
}
}
Adw.StatusPage status_page {
Adw.StatusPage {
icon-name: "music-note-symbolic";
title: _("Welcome to Musicus");
description: _("Get started by choosing where you want to store your music library. Are you using Musicus for the first time? If so, create a new empty folder for your library. If you wish, Musicus will automatically download some music for you.");
child: Gtk.Button {
styles ["suggested-action", "pill"]
styles [
"suggested-action",
"pill",
]
halign: center;
label: _("Choose library folder");
clicked => $choose_library_folder() swapped;
@ -34,8 +39,9 @@ menu primary_menu {
label: _("_Preferences");
action: "win.preferences";
}
item {
label: _("_About Musicus");
action: "app.about";
}
}
}

173
src/empty_page.rs Normal file
View file

@ -0,0 +1,173 @@
use std::cell::OnceCell;
use adw::{
prelude::*,
subclass::{navigation_page::NavigationPageImpl, prelude::*},
};
use gettextrs::gettext;
use glib::clone;
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,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/empty_page.blp")]
pub struct EmptyPage {
pub library: OnceCell<Library>,
pub process_manager: OnceCell<ProcessManager>,
#[template_child]
pub download_button: TemplateChild<gtk::Button>,
#[template_child]
pub process_list: TemplateChild<gtk::ListBox>,
}
#[glib::object_subclass]
impl ObjectSubclass for EmptyPage {
const NAME: &'static str = "MusicusEmptyPage";
type Type = super::EmptyPage;
type ParentType = adw::NavigationPage;
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 EmptyPage {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("ready").build()]);
SIGNALS.as_ref()
}
}
impl WidgetImpl for EmptyPage {}
impl NavigationPageImpl for EmptyPage {}
}
glib::wrapper! {
pub struct EmptyPage(ObjectSubclass<imp::EmptyPage>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl EmptyPage {
pub fn new(library: &Library, process_manager: &ProcessManager) -> Self {
let obj: Self = glib::Object::new();
for process in process_manager.processes() {
obj.add_process(&process);
}
obj.imp().library.set(library.to_owned()).unwrap();
obj.imp()
.process_manager
.set(process_manager.to_owned())
.unwrap();
obj
}
pub fn connect_ready<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("ready", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
#[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."))
.build();
dialog.add_response("continue", &gettext("Continue"));
dialog.set_response_appearance("continue", adw::ResponseAppearance::Suggested);
dialog.add_response("cancel", &gettext("Cancel"));
dialog.set_default_response(Some("cancel"));
dialog.set_close_response("cancel");
let obj = self.to_owned();
glib::spawn_future_local(async move {
if dialog.choose_future(&obj).await == "continue" {
obj.imp().download_button.set_visible(false);
let settings = gio::Settings::new(config::APP_ID);
let url = if settings.boolean("use-custom-library-url") {
settings.string("custom-library-url").to_string()
} else {
config::LIBRARY_URL.to_string()
};
match obj.imp().library.get().unwrap().import_url(&url) {
Ok(receiver) => {
let process = Process::new(&gettext("Downloading music library"), receiver);
process.connect_finished_notify(clone!(
#[weak]
obj,
move |process| {
if process.finished() {
if process.error().is_some() {
obj.imp().download_button.set_visible(true);
} else {
obj.emit_by_name::<()>("ready", &[]);
}
}
}
));
obj.imp()
.process_manager
.get()
.unwrap()
.add_process(&process);
obj.add_process(&process);
}
Err(err) => log::error!("Failed to download library: {err:?}"),
}
}
});
}
fn add_process(&self, process: &Process) {
let row = ProcessRow::new(process);
row.connect_remove(clone!(
#[weak(rename_to = obj)]
self,
move |row| {
obj.imp()
.process_manager
.get()
.unwrap()
.remove_process(&row.process());
obj.imp().process_list.remove(row);
if obj.imp().process_list.first_child().is_none() {
obj.imp().process_list.set_visible(false);
}
}
));
self.imp().process_list.append(&row);
self.imp().process_list.set_visible(true);
}
}

View file

@ -77,6 +77,16 @@ impl Library {
Ok(obj)
}
/// Whether this library is empty. The library is considered empty, if
/// there are no tracks.
pub fn is_empty(&self) -> Result<bool> {
let connection = &mut *self.imp().connection.get().unwrap().lock().unwrap();
Ok(tracks::table
.first::<tables::Track>(connection)
.optional()?
.is_none())
}
/// Import from a library archive.
pub fn import_archive(
&self,

View file

@ -4,6 +4,7 @@ mod application;
mod config;
mod db;
mod editor;
mod empty_page;
mod library;
mod library_manager;
mod player;

View file

@ -8,10 +8,7 @@ mod imp {
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/welcome_page.blp")]
pub struct WelcomePage {
#[template_child]
pub status_page: TemplateChild<adw::StatusPage>,
}
pub struct WelcomePage {}
#[glib::object_subclass]
impl ObjectSubclass for WelcomePage {

View file

@ -8,6 +8,7 @@ use gtk::{gio, glib, glib::clone};
use crate::{
config,
editor::tracks::TracksEditor,
empty_page::EmptyPage,
library::{Library, LibraryQuery},
library_manager::LibraryManager,
player::Player,
@ -259,8 +260,29 @@ impl Window {
));
self.imp().player.set_library(&library);
let is_empty = library.is_empty()?;
self.imp().library.replace(Some(library));
self.reset_view();
if is_empty {
let navigation = self.imp().navigation_view.get();
let empty_page = EmptyPage::new(
self.imp().library.borrow().as_ref().unwrap(),
&self.imp().process_manager,
);
empty_page.connect_ready(clone!(
#[weak(rename_to = obj)]
self,
move |_| {
obj.reset_view();
}
));
navigation.replace(&[empty_page.into()]);
} else {
self.reset_view();
}
Ok(())
}