Share UI between screens

The recording screen was reverted to a dummy in the process.
This commit is contained in:
Elias Projahn 2021-01-31 20:03:20 +01:00
parent 2d846a7b1a
commit 6abd450452
17 changed files with 555 additions and 977 deletions

View file

@ -2,20 +2,19 @@
<gresources> <gresources>
<gresource prefix="/de/johrpan/musicus"> <gresource prefix="/de/johrpan/musicus">
<file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file> <file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file> <file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file> <file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/medium_editor.ui</file> <file preprocess="xml-stripblanks">ui/medium_editor.ui</file>
<file preprocess="xml-stripblanks">ui/performance_editor.ui</file> <file preprocess="xml-stripblanks">ui/performance_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_editor.ui</file> <file preprocess="xml-stripblanks">ui/person_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_screen.ui</file>
<file preprocess="xml-stripblanks">ui/player_bar.ui</file> <file preprocess="xml-stripblanks">ui/player_bar.ui</file>
<file preprocess="xml-stripblanks">ui/player_screen.ui</file> <file preprocess="xml-stripblanks">ui/player_screen.ui</file>
<file preprocess="xml-stripblanks">ui/poe_list.ui</file> <file preprocess="xml-stripblanks">ui/poe_list.ui</file>
<file preprocess="xml-stripblanks">ui/preferences.ui</file> <file preprocess="xml-stripblanks">ui/preferences.ui</file>
<file preprocess="xml-stripblanks">ui/recording_editor.ui</file> <file preprocess="xml-stripblanks">ui/recording_editor.ui</file>
<file preprocess="xml-stripblanks">ui/recording_screen.ui</file>
<file preprocess="xml-stripblanks">ui/register_dialog.ui</file> <file preprocess="xml-stripblanks">ui/register_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/screen.ui</file>
<file preprocess="xml-stripblanks">ui/section.ui</file>
<file preprocess="xml-stripblanks">ui/selector.ui</file> <file preprocess="xml-stripblanks">ui/selector.ui</file>
<file preprocess="xml-stripblanks">ui/server_dialog.ui</file> <file preprocess="xml-stripblanks">ui/server_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/source_selector.ui</file> <file preprocess="xml-stripblanks">ui/source_selector.ui</file>
@ -25,7 +24,6 @@
<file preprocess="xml-stripblanks">ui/window.ui</file> <file preprocess="xml-stripblanks">ui/window.ui</file>
<file preprocess="xml-stripblanks">ui/work_editor.ui</file> <file preprocess="xml-stripblanks">ui/work_editor.ui</file>
<file preprocess="xml-stripblanks">ui/work_part_editor.ui</file> <file preprocess="xml-stripblanks">ui/work_part_editor.ui</file>
<file preprocess="xml-stripblanks">ui/work_screen.ui</file>
<file preprocess="xml-stripblanks">ui/work_section_editor.ui</file> <file preprocess="xml-stripblanks">ui/work_section_editor.ui</file>
</gresource> </gresource>
</gresources> </gresources>

View file

@ -1,126 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar" id="header">
<property name="title-widget">
<object class="GtkLabel" id="title_label">
<property name="label" translatable="yes">Ensemble</property>
<style>
<class name="title"/>
</style>
</object>
</property>
<child>
<object class="GtkButton" id="back_button">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">view-more-symbolic</property>
<property name="menu-model">menu</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search_button">
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSearchBar">
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
<child>
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="hexpand">true</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="placeholder-text" translatable="yes">Search recordings …</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner">
<property name="spinning">True</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">content</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<child>
<object class="AdwClamp">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">18</property>
<property name="margin-bottom">12</property>
<property name="maximum-size">800</property>
<child>
<object class="GtkBox" id="recording_box">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame" id="recording_frame">
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">nothing</property>
<property name="child">
<object class="GtkLabel">
<property name="label" translatable="yes">No recordings found.</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">Edit ensemble</attribute>
<attribute name="action">widget.edit</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Delete ensemble</attribute>
<attribute name="action">widget.delete</attribute>
</item>
</section>
</menu>
</interface>

View file

@ -1,155 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar" id="header">
<property name="title-widget">
<object class="GtkLabel" id="title_label">
<property name="label" translatable="yes">Person</property>
<style>
<class name="title"/>
</style>
</object>
</property>
<child>
<object class="GtkButton" id="back_button">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton">
<property name="menu-model">menu</property>
<property name="icon-name">view-more-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search_button">
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSearchBar">
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
<child>
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="hexpand">true</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="placeholder-text" translatable="yes">Search works and recordings …</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="spinning">true</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">content</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<child>
<object class="AdwClamp">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">18</property>
<property name="margin-bottom">12</property>
<property name="maximum-size">800</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkBox" id="work_box">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Works</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame" id="work_frame">
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="recording_box">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame" id="recording_frame">
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">nothing</property>
<property name="child">
<object class="GtkLabel">
<property name="label" translatable="yes">No works or recordings found.</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">Edit person</attribute>
<attribute name="action">widget.edit</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Delete person</attribute>
<attribute name="action">widget.delete</attribute>
</item>
</section>
</menu>
</interface>

View file

@ -1,127 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar" id="header">
<property name="title-widget">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="label" translatable="yes">Recording</property>
<style>
<class name="title"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="subtitle_label">
<style>
<class name="subtitle"/>
</style>
</object>
</child>
</object>
</property>
<child>
<object class="GtkButton" id="back_button">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">view-more-symbolic</property>
<property name="menu-model">menu</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner">
<property name="spinning">True</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">content</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<child>
<object class="AdwClamp">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">18</property>
<property name="margin-bottom">12</property>
<property name="maximum-size">800</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Tracks</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame" id="frame">
</object>
</child>
<child>
<object class="GtkButton" id="add_to_playlist_button">
<property name="label" translatable="yes">Add to playlist</property>
<property name="halign">end</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">Edit recording</attribute>
<attribute name="action">widget.edit</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Delete recording</attribute>
<attribute name="action">widget.delete</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Edit tracks</attribute>
<attribute name="action">widget.edit-tracks</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Delete tracks</attribute>
<attribute name="action">widget.delete-tracks</attribute>
</item>
</section>
</menu>
</interface>

86
res/ui/screen.ui Normal file
View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar">
<property name="title-widget">
<object class="AdwWindowTitle" id="window_title"/>
</property>
<child>
<object class="GtkButton" id="back_button">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton">
<property name="menu-model">menu</property>
<property name="icon-name">view-more-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search_button">
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSearchBar">
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
<child>
<object class="AdwClamp">
<property name="hexpand">true</property>
<child>
<object class="GtkSearchEntry" id="search_entry"/>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="width-request">32</property>
<property name="height-request">32</property>
<property name="spinning">true</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">content</property>
<property name="child">
<object class="GtkScrolledWindow">
<child>
<object class="AdwClamp">
<child>
<object class="GtkBox" id="content_box">
<property name="orientation">vertical</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-bottom">36</property>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
<menu id="menu"/>
</interface>

28
res/ui/section.ui Normal file
View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkBox" id="widget">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox" id="title_box">
<property name="spacing">12</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="ellipsize">end</property>
<property name="xalign">0.0</property>
<property name="valign">end</property>
<property name="hexpand">true</property>
<property name="margin-top">18</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
</child>
<child>
<object class="GtkFrame" id="frame"/>
</child>
</object>
</interface>

View file

@ -1,139 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar" id="header">
<property name="title-widget">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="label" translatable="yes">Work</property>
<style>
<class name="title"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="subtitle_label">
<style>
<class name="subtitle"/>
</style>
</object>
</child>
</object>
</property>
<child>
<object class="GtkButton" id="back_button">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">view-more-symbolic</property>
<property name="menu-model">menu</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search_button">
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSearchBar">
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
<child>
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="hexpand">true</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="placeholder-text" translatable="yes">Search recordings …</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner">
<property name="spinning">True</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">content</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<child>
<object class="AdwClamp">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">18</property>
<property name="margin-bottom">12</property>
<property name="maximum-size">800</property>
<child>
<object class="GtkBox" id="recording_box">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame" id="recording_frame">
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">nothing</property>
<property name="child">
<object class="GtkLabel">
<property name="label" translatable="yes">No recordings found.</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">Edit work</attribute>
<attribute name="action">widget.edit</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Delete work</attribute>
<attribute name="action">widget.delete</attribute>
</item>
</section>
</menu>
</interface>

View file

@ -77,12 +77,12 @@ sources = files(
'import/track_editor.rs', 'import/track_editor.rs',
'import/track_selector.rs', 'import/track_selector.rs',
'import/track_set_editor.rs', 'import/track_set_editor.rs',
'screens/ensemble_screen.rs', 'screens/ensemble.rs',
'screens/mod.rs', 'screens/mod.rs',
'screens/person_screen.rs', 'screens/person.rs',
'screens/player_screen.rs', 'screens/player_screen.rs',
'screens/recording_screen.rs', 'screens/recording.rs',
'screens/work_screen.rs', 'screens/work.rs',
'selectors/ensemble.rs', 'selectors/ensemble.rs',
'selectors/instrument.rs', 'selectors/instrument.rs',
'selectors/mod.rs', 'selectors/mod.rs',
@ -97,6 +97,8 @@ sources = files(
'widgets/navigator_window.rs', 'widgets/navigator_window.rs',
'widgets/player_bar.rs', 'widgets/player_bar.rs',
'widgets/poe_list.rs', 'widgets/poe_list.rs',
'widgets/screen.rs',
'widgets/section.rs',
'config.rs', 'config.rs',
'config.rs.in', 'config.rs.in',
'main.rs', 'main.rs',

View file

@ -1,74 +1,72 @@
use super::*; use super::RecordingScreen;
use crate::backend::*;
use crate::database::*; use crate::backend::Backend;
use crate::database::{Ensemble, Recording};
use crate::editors::EnsembleEditor; use crate::editors::EnsembleEditor;
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section};
use gio::prelude::*;
use gettextrs::gettext;
use glib::clone; use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*; use libadwaita::prelude::*;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
/// A screen for showing recordings with a ensemble.
pub struct EnsembleScreen { pub struct EnsembleScreen {
backend: Rc<Backend>, backend: Rc<Backend>,
ensemble: Ensemble, ensemble: Ensemble,
widget: gtk::Box, widget: Screen,
search_entry: gtk::SearchEntry,
stack: gtk::Stack,
recording_list: Rc<List>, recording_list: Rc<List>,
recordings: RefCell<Vec<Recording>>, recordings: RefCell<Vec<Recording>>,
navigator: RefCell<Option<Rc<Navigator>>>, navigator: RefCell<Option<Rc<Navigator>>>,
} }
impl EnsembleScreen { impl EnsembleScreen {
/// Create a new ensemble screen for the specified ensemble and load the
/// contents asynchronously.
pub fn new(backend: Rc<Backend>, ensemble: Ensemble) -> Rc<Self> { pub fn new(backend: Rc<Backend>, ensemble: Ensemble) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_screen.ui"); let widget = Screen::new();
widget.set_title(&ensemble.name);
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, recording_frame);
title_label.set_label(&ensemble.name);
let edit_action = gio::SimpleAction::new("edit", None);
let delete_action = gio::SimpleAction::new("delete", None);
let actions = gio::SimpleActionGroup::new();
actions.add_action(&edit_action);
actions.add_action(&delete_action);
widget.insert_action_group("widget", Some(&actions));
let recording_list = List::new(); let recording_list = List::new();
recording_frame.set_child(Some(&recording_list.widget));
let this = Rc::new(Self { let this = Rc::new(Self {
backend, backend,
ensemble, ensemble,
widget, widget,
search_entry,
stack,
recording_list, recording_list,
recordings: RefCell::new(Vec::new()), recordings: RefCell::new(Vec::new()),
navigator: RefCell::new(None), navigator: RefCell::new(None),
}); });
this.search_entry.connect_search_changed(clone!(@strong this => move |_| { this.widget.set_back_cb(clone!(@strong this => move || {
this.recording_list.invalidate_filter();
}));
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone(); let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator { if let Some(navigator) = navigator {
navigator.pop(); navigator.pop();
} }
})); }));
this.widget.add_action(&gettext("Edit ensemble"), clone!(@strong this => move || {
let editor = EnsembleEditor::new(this.backend.clone(), Some(this.ensemble.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
this.widget.add_action(&gettext("Delete ensemble"), clone!(@strong this => move || {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_ensemble(&clone.ensemble.id).await.unwrap();
clone.backend.library_changed();
});
}));
this.widget.set_search_cb(clone!(@strong this => move || {
this.recording_list.invalidate_filter();
}));
this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| { this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| {
let recording = &this.recordings.borrow()[index]; let recording = &this.recordings.borrow()[index];
@ -90,28 +88,16 @@ impl EnsembleScreen {
this.recording_list.set_filter_cb(clone!(@strong this => move |index| { this.recording_list.set_filter_cb(clone!(@strong this => move |index| {
let recording = &this.recordings.borrow()[index]; let recording = &this.recordings.borrow()[index];
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); let search = this.widget.get_search();
let text = recording.work.get_title() + &recording.get_performers(); let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.to_lowercase().contains(&search) search.is_empty() || text.to_lowercase().contains(&search)
})); }));
edit_action.connect_activate(clone!(@strong this => move |_, _| { // Load the content asynchronously.
let editor = EnsembleEditor::new(this.backend.clone(), Some(this.ensemble.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
delete_action.connect_activate(clone!(@strong this => move |_, _| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_ensemble(&clone.ensemble.id).await.unwrap();
clone.backend.library_changed();
});
}));
let context = glib::MainContext::default(); let context = glib::MainContext::default();
let clone = this.clone(); let clone = Rc::clone(&this);
context.spawn_local(async move { context.spawn_local(async move {
let recordings = clone let recordings = clone
.backend .backend
@ -120,14 +106,16 @@ impl EnsembleScreen {
.await .await
.unwrap(); .unwrap();
if recordings.is_empty() { if !recordings.is_empty() {
clone.stack.set_visible_child_name("nothing");
} else {
let length = recordings.len(); let length = recordings.len();
clone.recordings.replace(recordings); clone.recordings.replace(recordings);
clone.recording_list.update(length); clone.recording_list.update(length);
clone.stack.set_visible_child_name("content");
let section = Section::new("Recordings", &clone.recording_list.widget);
clone.widget.add_content(&section.widget);
} }
clone.widget.ready();
}); });
this this
@ -140,7 +128,7 @@ impl NavigatorScreen for EnsembleScreen {
} }
fn get_widget(&self) -> gtk::Widget { fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast() self.widget.widget.clone().upcast()
} }
fn detach_navigator(&self) { fn detach_navigator(&self) {

View file

@ -1,14 +1,14 @@
pub mod ensemble_screen; pub mod ensemble;
pub use ensemble_screen::*; pub use ensemble::*;
pub mod person_screen; pub mod person;
pub use person_screen::*; pub use person::*;
pub mod player_screen; pub mod player_screen;
pub use player_screen::*; pub use player_screen::*;
pub mod work_screen; pub mod work;
pub use work_screen::*; pub use work::*;
pub mod recording_screen; pub mod recording;
pub use recording_screen::*; pub use recording::*;

View file

@ -1,25 +1,23 @@
use super::*; use super::{WorkScreen, RecordingScreen};
use crate::backend::*;
use crate::database::*; use crate::backend::Backend;
use crate::database::{Person, Recording, Work};
use crate::editors::PersonEditor; use crate::editors::PersonEditor;
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section};
use gio::prelude::*;
use gettextrs::gettext;
use glib::clone; use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*; use libadwaita::prelude::*;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
/// A screen for showing works by and recordings with a person.
pub struct PersonScreen { pub struct PersonScreen {
backend: Rc<Backend>, backend: Rc<Backend>,
person: Person, person: Person,
widget: gtk::Box, widget: Screen,
stack: gtk::Stack,
search_entry: gtk::SearchEntry,
work_box: gtk::Box,
work_list: Rc<List>, work_list: Rc<List>,
recording_box: gtk::Box,
recording_list: Rc<List>, recording_list: Rc<List>,
works: RefCell<Vec<Work>>, works: RefCell<Vec<Work>>,
recordings: RefCell<Vec<Recording>>, recordings: RefCell<Vec<Recording>>,
@ -27,62 +25,54 @@ pub struct PersonScreen {
} }
impl PersonScreen { impl PersonScreen {
/// Create a new person screen for the specified person and load the
/// contents asynchronously.
pub fn new(backend: Rc<Backend>, person: Person) -> Rc<Self> { pub fn new(backend: Rc<Backend>, person: Person) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_screen.ui"); let widget = Screen::new();
widget.set_title(&person.name_fl());
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Box, work_box);
get_widget!(builder, gtk::Frame, work_frame);
get_widget!(builder, gtk::Box, recording_box);
get_widget!(builder, gtk::Frame, recording_frame);
title_label.set_label(&person.name_fl());
let edit_action = gio::SimpleAction::new("edit", None);
let delete_action = gio::SimpleAction::new("delete", None);
let actions = gio::SimpleActionGroup::new();
actions.add_action(&edit_action);
actions.add_action(&delete_action);
widget.insert_action_group("widget", Some(&actions));
let work_list = List::new(); let work_list = List::new();
let recording_list = List::new(); let recording_list = List::new();
work_frame.set_child(Some(&work_list.widget));
recording_frame.set_child(Some(&recording_list.widget));
let this = Rc::new(Self { let this = Rc::new(Self {
backend, backend,
person, person,
widget, widget,
stack,
search_entry,
work_box,
work_list, work_list,
recording_box,
recording_list, recording_list,
works: RefCell::new(Vec::new()), works: RefCell::new(Vec::new()),
recordings: RefCell::new(Vec::new()), recordings: RefCell::new(Vec::new()),
navigator: RefCell::new(None), navigator: RefCell::new(None),
}); });
this.search_entry.connect_search_changed(clone!(@strong this => move |_| { this.widget.set_back_cb(clone!(@strong this => move || {
this.work_list.invalidate_filter();
this.recording_list.invalidate_filter();
}));
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone(); let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator { if let Some(navigator) = navigator {
navigator.clone().pop(); navigator.pop();
} }
})); }));
this.widget.add_action(&gettext("Edit person"), clone!(@strong this => move || {
let editor = PersonEditor::new(this.backend.clone(), Some(this.person.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
this.widget.add_action(&gettext("Delete person"), clone!(@strong this => move || {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_person(&clone.person.id).await.unwrap();
clone.backend.library_changed();
});
}));
this.widget.set_search_cb(clone!(@strong this => move || {
this.work_list.invalidate_filter();
this.recording_list.invalidate_filter();
}));
this.work_list.set_make_widget_cb(clone!(@strong this => move |index| { this.work_list.set_make_widget_cb(clone!(@strong this => move |index| {
let work = &this.works.borrow()[index]; let work = &this.works.borrow()[index];
@ -103,7 +93,7 @@ impl PersonScreen {
this.work_list.set_filter_cb(clone!(@strong this => move |index| { this.work_list.set_filter_cb(clone!(@strong this => move |index| {
let work = &this.works.borrow()[index]; let work = &this.works.borrow()[index];
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); let search = this.widget.get_search();
let title = work.title.to_lowercase(); let title = work.title.to_lowercase();
search.is_empty() || title.contains(&search) search.is_empty() || title.contains(&search)
})); }));
@ -129,28 +119,16 @@ impl PersonScreen {
this.recording_list.set_filter_cb(clone!(@strong this => move |index| { this.recording_list.set_filter_cb(clone!(@strong this => move |index| {
let recording = &this.recordings.borrow()[index]; let recording = &this.recordings.borrow()[index];
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); let search = this.widget.get_search();
let text = recording.work.get_title() + &recording.get_performers(); let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search) search.is_empty() || text.to_lowercase().contains(&search)
})); }));
edit_action.connect_activate(clone!(@strong this => move |_, _| { // Load the content asynchronously.
let editor = PersonEditor::new(this.backend.clone(), Some(this.person.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
delete_action.connect_activate(clone!(@strong this => move |_, _| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_person(&clone.person.id).await.unwrap();
clone.backend.library_changed();
});
}));
let context = glib::MainContext::default(); let context = glib::MainContext::default();
let clone = this.clone(); let clone = Rc::clone(&this);
context.spawn_local(async move { context.spawn_local(async move {
let works = clone let works = clone
.backend .backend
@ -166,27 +144,25 @@ impl PersonScreen {
.await .await
.unwrap(); .unwrap();
if works.is_empty() && recordings.is_empty() { if !works.is_empty() {
clone.stack.set_visible_child_name("nothing");
} else {
if works.is_empty() {
clone.work_box.hide();
} else {
let length = works.len(); let length = works.len();
clone.works.replace(works); clone.works.replace(works);
clone.work_list.update(length); clone.work_list.update(length);
let section = Section::new("Works", &clone.work_list.widget);
clone.widget.add_content(&section.widget);
} }
if recordings.is_empty() { if !recordings.is_empty() {
clone.recording_box.hide();
} else {
let length = recordings.len(); let length = recordings.len();
clone.recordings.replace(recordings); clone.recordings.replace(recordings);
clone.recording_list.update(length); clone.recording_list.update(length);
let section = Section::new("Recordings", &clone.recording_list.widget);
clone.widget.add_content(&section.widget);
} }
clone.stack.set_visible_child_name("content"); clone.widget.ready();
}
}); });
this this
@ -199,7 +175,7 @@ impl NavigatorScreen for PersonScreen {
} }
fn get_widget(&self) -> gtk::Widget { fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast() self.widget.widget.clone().upcast()
} }
fn detach_navigator(&self) { fn detach_navigator(&self) {

106
src/screens/recording.rs Normal file
View file

@ -0,0 +1,106 @@
use crate::backend::Backend;
use crate::database::Recording;
use crate::editors::RecordingEditor;
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for showing a recording.
pub struct RecordingScreen {
backend: Rc<Backend>,
recording: Recording,
widget: Screen,
track_list: Rc<List>,
recordings: RefCell<Vec<Recording>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingScreen {
/// Create a new recording screen for the specified recording and load the
/// contents asynchronously.
pub fn new(backend: Rc<Backend>, recording: Recording) -> Rc<Self> {
let widget = Screen::new();
widget.set_title(&recording.work.get_title());
widget.set_subtitle(&recording.get_performers());
let track_list = List::new();
let this = Rc::new(Self {
backend,
recording,
widget,
track_list,
recordings: RefCell::new(Vec::new()),
navigator: RefCell::new(None),
});
this.widget.set_back_cb(clone!(@strong this => move || {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.widget.add_action(&gettext("Edit recording"), clone!(@strong this => move || {
let editor = RecordingEditor::new(this.backend.clone(), Some(this.recording.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
this.widget.add_action(&gettext("Delete recording"), clone!(@strong this => move || {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_recording(&clone.recording.id).await.unwrap();
clone.backend.library_changed();
});
}));
this.widget.set_search_cb(clone!(@strong this => move || {
this.track_list.invalidate_filter();
}));
// TODO: Implement.
// this.track_list.set_make_widget_cb(clone!(@strong this => move |index| {
// }));
this.track_list.set_filter_cb(clone!(@strong this => move |index| {
// TODO: Implement.
// search.is_empty() || text.to_lowercase().contains(&search)
true
}));
// Load the content asynchronously.
let context = glib::MainContext::default();
let clone = Rc::clone(&this);
context.spawn_local(async move {
// TODO: Implement.
clone.widget.ready();
});
this
}
}
impl NavigatorScreen for RecordingScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,213 +0,0 @@
use crate::backend::*;
use crate::database::*;
use crate::editors::RecordingEditor;
use crate::player::*;
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
use gettextrs::gettext;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// Representation of one entry within the track list.
enum ListItem {
/// A track row. This hold an index to the track set and an index to the
/// track within the track set.
Track(usize, usize),
/// A separator intended for use between track sets.
Separator,
}
pub struct RecordingScreen {
backend: Rc<Backend>,
recording: Recording,
widget: gtk::Box,
stack: gtk::Stack,
list: Rc<List>,
track_sets: RefCell<Vec<TrackSet>>,
items: RefCell<Vec<ListItem>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingScreen {
pub fn new(backend: Rc<Backend>, recording: Recording) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Label, subtitle_label);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, frame);
get_widget!(builder, gtk::Button, add_to_playlist_button);
title_label.set_label(&recording.work.get_title());
subtitle_label.set_label(&recording.get_performers());
let edit_action = gio::SimpleAction::new("edit", None);
let delete_action = gio::SimpleAction::new("delete", None);
let edit_tracks_action = gio::SimpleAction::new("edit-tracks", None);
let delete_tracks_action = gio::SimpleAction::new("delete-tracks", None);
let actions = gio::SimpleActionGroup::new();
actions.add_action(&edit_action);
actions.add_action(&delete_action);
actions.add_action(&edit_tracks_action);
actions.add_action(&delete_tracks_action);
widget.insert_action_group("widget", Some(&actions));
let list = List::new();
frame.set_child(Some(&list.widget));
let this = Rc::new(Self {
backend,
recording,
widget,
stack,
list,
track_sets: RefCell::new(Vec::new()),
items: RefCell::new(Vec::new()),
navigator: RefCell::new(None),
});
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
match this.items.borrow()[index] {
ListItem::Track(track_set_index, track_index) => {
let track_set = &this.track_sets.borrow()[track_set_index];
let track = &track_set.tracks[track_index];
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
title_parts.push(this.recording.work.parts[*part].title.clone());
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let row = libadwaita::ActionRow::new();
row.set_title(Some(&title));
row.upcast()
}
ListItem::Separator => {
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
separator.upcast()
}
}
}));
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}));
// TODO: Decide whether to handle multiple track sets.
add_to_playlist_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = this.backend.get_player() {
if let Some(track_set) = this.track_sets.borrow().get(0).cloned() {
let indices = (0..track_set.tracks.len()).collect();
let playlist_item = PlaylistItem {
track_set,
indices,
};
player.add_item(playlist_item).unwrap();
}
}
}));
edit_action.connect_activate(clone!(@strong this => move |_, _| {
let editor = RecordingEditor::new(this.backend.clone(), Some(this.recording.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
delete_action.connect_activate(clone!(@strong this => move |_, _| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_recording(&clone.recording.id).await.unwrap();
clone.backend.library_changed();
});
}));
edit_tracks_action.connect_activate(clone!(@strong this => move |_, _| {
// let editor = TracksEditor::new(this.backend.clone(), Some(this.recording.clone()), this.tracks.borrow().clone());
// let window = NavigatorWindow::new(editor);
// window.show();
}));
delete_tracks_action.connect_activate(clone!(@strong this => move |_, _| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
// clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
// clone.backend.library_changed();
});
}));
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let track_sets = clone
.backend
.db()
.get_track_sets(&clone.recording.id)
.await
.unwrap();
clone.show_track_sets(track_sets);
clone.stack.set_visible_child_name("content");
});
this
}
/// Update the track sets variable as well as the user interface.
fn show_track_sets(&self, track_sets: Vec<TrackSet>) {
let mut first = true;
let mut items = Vec::new();
for (track_set_index, track_set) in track_sets.iter().enumerate() {
if !first {
items.push(ListItem::Separator);
} else {
first = false;
}
for (track_index, _) in track_set.tracks.iter().enumerate() {
items.push(ListItem::Track(track_set_index, track_index));
}
}
let length = items.len();
self.items.replace(items);
self.track_sets.replace(track_sets);
self.list.update(length);
}
}
impl NavigatorScreen for RecordingScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,76 +1,73 @@
use super::*; use super::RecordingScreen;
use crate::backend::*;
use crate::database::*; use crate::backend::Backend;
use crate::database::{Work, Recording};
use crate::editors::WorkEditor; use crate::editors::WorkEditor;
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section};
use gio::prelude::*;
use gettextrs::gettext;
use glib::clone; use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*; use libadwaita::prelude::*;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
/// A screen for showing recordings of a work.
pub struct WorkScreen { pub struct WorkScreen {
backend: Rc<Backend>, backend: Rc<Backend>,
work: Work, work: Work,
widget: gtk::Box, widget: Screen,
stack: gtk::Stack,
search_entry: gtk::SearchEntry,
recording_list: Rc<List>, recording_list: Rc<List>,
recordings: RefCell<Vec<Recording>>, recordings: RefCell<Vec<Recording>>,
navigator: RefCell<Option<Rc<Navigator>>>, navigator: RefCell<Option<Rc<Navigator>>>,
} }
impl WorkScreen { impl WorkScreen {
/// Create a new work screen for the specified work and load the
/// contents asynchronously.
pub fn new(backend: Rc<Backend>, work: Work) -> Rc<Self> { pub fn new(backend: Rc<Backend>, work: Work) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui"); let widget = Screen::new();
widget.set_title(&work.title);
get_widget!(builder, gtk::Box, widget); widget.set_subtitle(&work.composer.name_fl());
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Label, subtitle_label);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, recording_frame);
title_label.set_label(&work.composer.name_fl());
subtitle_label.set_label(&work.title);
let edit_action = gio::SimpleAction::new("edit", None);
let delete_action = gio::SimpleAction::new("delete", None);
let actions = gio::SimpleActionGroup::new();
actions.add_action(&edit_action);
actions.add_action(&delete_action);
widget.insert_action_group("widget", Some(&actions));
let recording_list = List::new(); let recording_list = List::new();
recording_frame.set_child(Some(&recording_list.widget));
let this = Rc::new(Self { let this = Rc::new(Self {
backend, backend,
work, work,
widget, widget,
stack,
search_entry,
recording_list, recording_list,
recordings: RefCell::new(Vec::new()), recordings: RefCell::new(Vec::new()),
navigator: RefCell::new(None), navigator: RefCell::new(None),
}); });
this.search_entry.connect_search_changed(clone!(@strong this => move |_| { this.widget.set_back_cb(clone!(@strong this => move || {
this.recording_list.invalidate_filter();
}));
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone(); let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator { if let Some(navigator) = navigator {
navigator.clone().pop(); navigator.pop();
} }
})); }));
this.widget.add_action(&gettext("Edit work"), clone!(@strong this => move || {
let editor = WorkEditor::new(this.backend.clone(), Some(this.work.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
this.widget.add_action(&gettext("Delete work"), clone!(@strong this => move || {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_work(&clone.work.id).await.unwrap();
clone.backend.library_changed();
});
}));
this.widget.set_search_cb(clone!(@strong this => move || {
this.recording_list.invalidate_filter();
}));
this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| { this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| {
let recording = &this.recordings.borrow()[index]; let recording = &this.recordings.borrow()[index];
@ -92,28 +89,16 @@ impl WorkScreen {
this.recording_list.set_filter_cb(clone!(@strong this => move |index| { this.recording_list.set_filter_cb(clone!(@strong this => move |index| {
let recording = &this.recordings.borrow()[index]; let recording = &this.recordings.borrow()[index];
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); let search = this.widget.get_search();
let text = recording.work.get_title() + &recording.get_performers(); let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.to_lowercase().contains(&search) search.is_empty() || text.to_lowercase().contains(&search)
})); }));
edit_action.connect_activate(clone!(@strong this => move |_, _| { // Load the content asynchronously.
let editor = WorkEditor::new(this.backend.clone(), Some(this.work.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
delete_action.connect_activate(clone!(@strong this => move |_, _| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.backend.db().delete_work(&clone.work.id).await.unwrap();
clone.backend.library_changed();
});
}));
let context = glib::MainContext::default(); let context = glib::MainContext::default();
let clone = this.clone(); let clone = Rc::clone(&this);
context.spawn_local(async move { context.spawn_local(async move {
let recordings = clone let recordings = clone
.backend .backend
@ -122,14 +107,16 @@ impl WorkScreen {
.await .await
.unwrap(); .unwrap();
if recordings.is_empty() { if !recordings.is_empty() {
clone.stack.set_visible_child_name("nothing");
} else {
let length = recordings.len(); let length = recordings.len();
clone.recordings.replace(recordings); clone.recordings.replace(recordings);
clone.recording_list.update(length); clone.recording_list.update(length);
clone.stack.set_visible_child_name("content");
let section = Section::new("Recordings", &clone.recording_list.widget);
clone.widget.add_content(&section.widget);
} }
clone.widget.ready();
}); });
this this
@ -142,7 +129,7 @@ impl NavigatorScreen for WorkScreen {
} }
fn get_widget(&self) -> gtk::Widget { fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast() self.widget.widget.clone().upcast()
} }
fn detach_navigator(&self) { fn detach_navigator(&self) {

View file

@ -13,4 +13,10 @@ pub use player_bar::*;
pub mod poe_list; pub mod poe_list;
pub use poe_list::*; pub use poe_list::*;
pub mod screen;
pub use screen::*;
pub mod section;
pub use section::*;
mod indexed_list_model; mod indexed_list_model;

113
src/widgets/screen.rs Normal file
View file

@ -0,0 +1,113 @@
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
/// A general framework for screens. Screens have a header bar with at least
/// a button to go back and a scrollable content area that clamps its content.
pub struct Screen {
/// The actual GTK widget.
pub widget: gtk::Box,
/// The button to switch to the previous screen.
back_button: gtk::Button,
/// The title widget within the header bar.
window_title: libadwaita::WindowTitle,
/// The action menu.
menu: gio::Menu,
/// The entry for searching.
search_entry: gtk::SearchEntry,
/// The stack to switch to the loading page.
stack: gtk::Stack,
/// The box containing the content.
content_box: gtk::Box,
/// The actions for the menu.
actions: gio::SimpleActionGroup,
}
impl Screen {
/// Create a new screen.
pub fn new() -> Self {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, libadwaita::WindowTitle, window_title);
get_widget!(builder, gio::Menu, menu);
get_widget!(builder, gtk::ToggleButton, search_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Box, content_box);
let actions = gio::SimpleActionGroup::new();
widget.insert_action_group("widget", Some(&actions));
search_button.connect_toggled(clone!(@strong search_entry => move |search_button| {
if search_button.get_active() {
search_entry.grab_focus();
}
}));
Self {
widget,
back_button,
window_title,
menu,
search_entry,
stack,
content_box,
actions,
}
}
/// Set a closure to be called when the back button is pressed.
pub fn set_back_cb<F: Fn() + 'static>(&self, cb: F) {
self.back_button.connect_clicked(move |_| cb());
}
/// Show a title in the header bar.
pub fn set_title(&self, title: &str) {
self.window_title.set_title(Some(title));
}
/// Show a subtitle in the header bar.
pub fn set_subtitle(&self, subtitle: &str) {
self.window_title.set_subtitle(Some(subtitle));
}
/// Add a new item to the action menu and register a callback for it.
pub fn add_action<F: Fn() + 'static>(&self, label: &str, cb: F) {
let name = rand::random::<u64>().to_string();
let action = gio::SimpleAction::new(&name, None);
action.connect_activate(move |_, _| cb());
self.actions.add_action(&action);
self.menu.append(Some(label), Some(&format!("widget.{}", name)));
}
/// Set the closure to be called when the search string has changed.
pub fn set_search_cb<F: Fn() + 'static>(&self, cb: F) {
self.search_entry.connect_search_changed(move |_| cb());
}
/// Get the current search string.
pub fn get_search(&self) -> String {
self.search_entry.get_text().unwrap().to_string().to_lowercase()
}
/// Hide the loading page and switch to the content.
pub fn ready(&self) {
self.stack.set_visible_child_name("content");
}
/// Add content to the bottom of the content area.
pub fn add_content<W: IsA<gtk::Widget>>(&self, content: &W) {
self.content_box.append(content);
}
}

48
src/widgets/section.rs Normal file
View file

@ -0,0 +1,48 @@
use gtk::prelude::*;
use gtk_macros::get_widget;
/// A widget displaying a title, a framed child widget and, if needed, some
/// actions.
pub struct Section {
/// The actual GTK widget.
pub widget: gtk::Box,
/// The box containing the title and action buttons.
title_box: gtk::Box,
}
impl Section {
/// Create a new section.
pub fn new<W: IsA<gtk::Widget>>(title: &str, content: &W) -> Self {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Box, title_box);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Frame, frame);
title_label.set_label(title);
frame.set_child(Some(content));
Self {
widget,
title_box,
}
}
/// Add an action button. This should by definition be something that is
/// doing something with the child widget that is applicable in all
/// situations where the widget is visible. The new button will be packed
/// to the end of the title box.
pub fn add_action<F: Fn() + 'static>(&self, icon_name: &str, cb: F) {
let button = gtk::ButtonBuilder::new()
.has_frame(false)
.valign(gtk::Align::Center)
.icon_name(icon_name)
.build();
button.connect_clicked(move |_| cb());
self.title_box.append(&button);
}
}