Revert merging of server and client repository

This commit is contained in:
Elias Projahn 2021-01-16 16:15:08 +01:00
parent 2b9cff885b
commit 8c3c439409
147 changed files with 53 additions and 2113 deletions

10
musicus/.gitignore vendored
View file

@ -1,10 +0,0 @@
/.flatpak-builder
/.vscode
/build
/builddir
/flatpak
/src/config.rs
/src/resources.rs
/target
Cargo.lock
test.sqlite

View file

@ -1,29 +0,0 @@
[package]
name = "musicus"
version = "0.1.0"
edition = "2018"
[dependencies]
anyhow = "1.0.33"
diesel = { version = "1.4.5", features = ["sqlite"] }
diesel_migrations = "1.4.0"
discid = "0.4.4"
fragile = "1.0.0"
futures = "0.3.6"
futures-channel = "0.3.5"
gdk = "0.13.2"
gettext-rs = "0.5.0"
gio = "0.9.1"
glib = "0.10.2"
gtk = { version = "0.9.2", features = ["v3_24"] }
gtk-macros = "0.2.0"
gstreamer = "0.16.4"
gstreamer-player = "0.16.3"
isahc = "0.9.12"
libhandy = "0.7.0"
pango = "0.9.1"
rand = "0.7.3"
secret-service = "1.1.1"
serde = { version = "1.0.117", features = ["derive"] }
serde_json = "1.0.59"
uuid = { version = "0.8", features = ["v4"] }

View file

@ -1,75 +0,0 @@
# Musicus
This is a desktop app for Musicus.
https://musicus.org
## Hacking
### Building
Musicus uses the [Meson build system](https://mesonbuild.com/). You can build
it using the following commands:
```
$ meson build
$ ninja -C build
```
Afterwards the resulting binary executable is under
`build/target/debug/musicus`.
### Flatpak
There is a Flatpak manifest file called `de.johrpan.musicus.json`. To build a
Flatpak you need the the latest Gnome SDK and the Freedesktop SDK with the Rust
extension. You can install those using the following commands:
```
$ flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
$ flatpak remote-add --user --if-not-exists gnome-nightly https://nightly.gnome.org/gnome-nightly.flatpakrepo
$ flatpak install --user gnome-nightly org.gnome.Sdk org.gnome.Platform
$ flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//19.08
```
Afterwards, the following commands will build, install and run the application:
```
$ rm -rf flatpak
$ flatpak-builder --user --install flatpak de.johrpan.musicus.json
$ flatpak run de.johrpan.musicus
```
### Special requirements
This program uses [Diesel](https://diesel.rs) as its ORM. After installing
the Diesel command line utility, you will be able to create a new schema
migration using the following command:
```
$ diesel migration generate [change_description]
```
To update the `src/database/schema.rs` file, you should use the following
command:
```
$ diesel migration run --database-url test.sqlite
```
This file should never be edited manually.
## License
Musicus is free and open source software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your option)
any later version.
Musicus is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see https://www.gnu.org/licenses/.

View file

@ -1,24 +0,0 @@
#!/bin/sh
export MESON_BUILD_ROOT="$1"
export MESON_SOURCE_ROOT="$2"
export CARGO_TARGET_DIR="$MESON_BUILD_ROOT"/target
export CARGO_HOME="$CARGO_TARGET_DIR"/cargo-home
export OUTPUT="$3"
export BUILDTYPE="$4"
export APP_BIN="$5"
if [ $BUILDTYPE = "release" ]
then
echo "RELEASE MODE"
cargo build --manifest-path \
"$MESON_SOURCE_ROOT"/Cargo.toml --release && \
cp "$CARGO_TARGET_DIR"/release/"$APP_BIN" "$OUTPUT"
else
echo "DEBUG MODE"
cargo build --manifest-path \
"$MESON_SOURCE_ROOT"/Cargo.toml --verbose && \
cp "$CARGO_TARGET_DIR"/debug/"$APP_BIN" "$OUTPUT"
fi

View file

@ -1,21 +0,0 @@
#!/usr/bin/env python3
from os import environ, path
from subprocess import call
prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
datadir = path.join(prefix, 'share')
destdir = environ.get('DESTDIR', '')
# Package managers set this so we don't need to run
if not destdir:
print('Updating icon cache...')
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
print('Updating desktop database...')
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
print('Compiling GSettings schemas...')
call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])

View file

@ -1,8 +0,0 @@
[Desktop Entry]
Name=Musicus
Icon=de.johrpan.musicus
Exec=musicus
Terminal=false
Type=Application
Categories=GTK;
StartupNotify=true

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="musicus">
<schema id="de.johrpan.musicus" path="/de/johrpan/musicus/">
<key name="music-library-path" type="s">
<default>""</default>
<summary>Path to the music library folder</summary>
</key>
<key name="server-url" type="s">
<default>"https://musicus.johrpan.de"</default>
<summary>URL of the Musicus server to use</summary>
</key>
</schema>
</schemalist>

View file

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="musicus.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.6046466"
inkscape:cx="31.109369"
inkscape:cy="42.493912"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
inkscape:window-width="1396"
inkscape:window-height="1016"
inkscape:window-x="50"
inkscape:window-y="27"
inkscape:window-maximized="0"
units="px">
<inkscape:grid
type="xygrid"
id="grid851"
spacingx="1.0583333"
spacingy="1.0583333"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#303030;fill-opacity:1;stroke:none;stroke-width:0.2;stroke-opacity:0.5;opacity:1"
id="path853"
cx="16.933332"
cy="16.933332"
r="15.874999" />
<path
d="M 13.553183,9.5568091 C 13.056762,12.40878 12.41526,15.765311 11.641666,19.579166 11.112514,19.117684 10.363554,18.82791 9.5303854,18.82791 c -1.6023905,0 -2.9013699,1.06575 -2.9013699,2.380533 0,1.314781 1.2989794,2.380531 2.9013699,2.380531 1.6023916,0 2.6414696,-0.72268 2.9013706,-2.380531 0.371458,-2.606532 0.693542,-4.484063 1.272743,-8.053734 h 0.08454 c 0.97349,2.045774 1.97525,4.277025 3.004977,6.691673 l 1.051893,-2.149423 c -1.146154,-2.433005 -2.36032,-5.134786 -3.650597,-8.1403239 z m 9.830992,0 c -1.922644,3.9360329 -4.028599,8.2312419 -5.872099,11.9996389 0.214174,0.478841 0.213832,0.473733 0.706271,0.806455 1.391414,-3.185731 2.821015,-6.23621 4.287978,-9.15126 l 0.07567,0.0096 c 0.497372,3.746054 0.852824,6.812033 1.066876,9.198452 0.195174,-0.03778 0.513002,-0.05658 0.953701,-0.05658 0.52257,0 0.881268,0.01883 1.076442,0.05658 -0.705145,-4.230869 -1.308981,-8.518284 -1.812675,-12.8625029 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ffc107;fill-opacity:1;stroke-width:0.349035" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 4.2333332 4.2333332"
version="1.1"
id="svg2197"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="musicus_symbolic.svg">
<defs
id="defs2191" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="31.083523"
inkscape:cx="9.8959183"
inkscape:cy="9.0432761"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
inkscape:window-width="1396"
inkscape:window-height="1016"
inkscape:window-x="236"
inkscape:window-y="185"
inkscape:window-maximized="0"
units="px">
<inkscape:grid
type="xygrid"
id="grid2786"
spacingx="0.52916665"
spacingy="0.52916665"
empspacing="2" />
</sodipodi:namedview>
<metadata
id="metadata2194">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
d="M 1.6109484,0.75244585 C 1.5144212,1.3069954 1.3896858,1.9596541 1.2392649,2.7012366 1.1363746,2.6115036 0.99074332,2.5551592 0.82873813,2.5551592 c -0.31157571,0 -0.56415494,0.207229 -0.56415494,0.4628812 0,0.2556517 0.25257923,0.4628809 0.56415494,0.4628809 0.31157607,0 0.51361887,-0.1405212 0.56415527,-0.4628809 C 1.4651224,2.511215 1.5277477,2.1461397 1.6403711,1.4520373 h 0.016433 c 0.1892954,0.397789 0.3840822,0.8316432 0.5843067,1.3011582 L 2.4456455,2.3352522 C 2.2227823,1.8621681 1.9866947,1.3368218 1.7358073,0.7524119 Z m 1.9115815,0 C 3.1486824,1.5177853 2.7391913,2.3529645 2.380733,3.0857081 2.42238,3.1788171 2.422311,3.1778251 2.5180621,3.2425184 2.7886154,2.6230712 3.0665935,2.0299227 3.3518361,1.4631079 l 0.014716,0.00186 c 0.09671,0.7283991 0.1658266,1.3245612 0.2074481,1.7885873 0.037951,-0.00736 0.09975,-0.011014 0.1854421,-0.011014 0.1016105,0 0.1713582,0.00368 0.2093077,0.011014 C 3.8316365,2.4308883 3.7142245,1.5972244 3.6162839,0.7525155 Z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;stroke-width:0.0678681" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,41 +0,0 @@
datadir = get_option('datadir')
scalable_dir = join_paths('icons', 'hicolor', 'scalable', 'apps')
install_data(
join_paths(scalable_dir, 'de.johrpan.musicus.svg'),
install_dir: join_paths(datadir, scalable_dir),
)
symbolic_dir = join_paths('icons', 'hicolor', 'symbolic', 'apps')
install_data(
join_paths(symbolic_dir, 'de.johrpan.musicus-symbolic.svg'),
install_dir: join_paths(datadir, symbolic_dir),
)
desktop_file = i18n.merge_file(
input: 'de.johrpan.musicus.desktop.in',
output: 'de.johrpan.musicus.desktop',
type: 'desktop',
po_dir: '../po',
install: true,
install_dir: join_paths(datadir, 'applications')
)
desktop_utils = find_program('desktop-file-validate', required: false)
if desktop_utils.found()
test('Validate desktop file', desktop_utils,
args: [desktop_file]
)
endif
install_data('de.johrpan.musicus.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
args: ['--strict', '--dry-run', meson.current_source_dir()]
)
endif

View file

@ -1,53 +0,0 @@
{
"app-id" : "de.johrpan.musicus",
"runtime" : "org.gnome.Platform",
"runtime-version" : "master",
"sdk" : "org.gnome.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command" : "musicus",
"finish-args" : [
"--share=network",
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--socket=pulseaudio",
"--filesystem=host"
],
"build-options" : {
"append-path" : "/usr/lib/sdk/rust-stable/bin",
"build-args" : [
"--share=network"
],
"env" : {
"CARGO_HOME" : "/run/build/musicus/cargo",
"RUST_BACKTRACE" : "1",
"RUST_LOG" : "musicus=debug"
}
},
"cleanup" : [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"*.la",
"*.a"
],
"modules" : [
{
"name" : "musicus",
"builddir" : true,
"buildsystem" : "meson",
"sources" : [
{
"type" : "git",
"url" : "."
}
]
}
]
}

View file

@ -1,2 +0,0 @@
[print_schema]
file = "src/database/schema.rs"

View file

@ -1,24 +0,0 @@
project('musicus', 'rust',
version: '0.1.0',
meson_version: '>= 0.50.0',
license: 'AGPLv3+',
)
dependency('dbus-1', version: '>= 1.3')
dependency('glib-2.0', version: '>= 2.56')
dependency('gio-2.0', version: '>= 2.56')
dependency('gstreamer-1.0', version: '>= 1.12')
dependency('gtk+-3.0', version: '>= 3.24.7')
dependency('libcurl', version: '>= 7.24.0')
dependency('libhandy-1', version: '>= 1.0.0')
dependency('pango', version: '>= 1.0')
dependency('sqlite3', version: '>= 3.20')
i18n = import('i18n')
subdir('data')
subdir('po')
subdir('res')
subdir('src')
meson.add_install_script('build-aux/postinstall.py')

View file

@ -1,15 +0,0 @@
PRAGMA defer_foreign_keys;
DROP TABLE "persons";
DROP TABLE "instruments";
DROP TABLE "works";
DROP TABLE "instrumentations";
DROP TABLE "work_parts";
DROP TABLE "work_sections";
DROP TABLE "ensembles";
DROP TABLE "recordings";
DROP TABLE "performances";
DROP TABLE "mediums";
DROP TABLE "track_sets";
DROP TABLE "tracks";

View file

@ -1,78 +0,0 @@
CREATE TABLE "persons" (
"id" TEXT NOT NULL PRIMARY KEY,
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL
);
CREATE TABLE "instruments" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL
);
CREATE TABLE "works" (
"id" TEXT NOT NULL PRIMARY KEY,
"composer" TEXT NOT NULL REFERENCES "persons"("id"),
"title" TEXT NOT NULL
);
CREATE TABLE "instrumentations" (
"id" BIGINT NOT NULL PRIMARY KEY,
"work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
"instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE
);
CREATE TABLE "work_parts" (
"id" BIGINT NOT NULL PRIMARY KEY,
"work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
"part_index" BIGINT NOT NULL,
"title" TEXT NOT NULL,
"composer" TEXT REFERENCES "persons"("id")
);
CREATE TABLE "work_sections" (
"id" BIGINT NOT NULL PRIMARY KEY,
"work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
"title" TEXT NOT NULL,
"before_index" BIGINT NOT NULL
);
CREATE TABLE "ensembles" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL
);
CREATE TABLE "recordings" (
"id" TEXT NOT NULL PRIMARY KEY,
"work" TEXT NOT NULL REFERENCES "works"("id"),
"comment" TEXT NOT NULL
);
CREATE TABLE "performances" (
"id" BIGINT NOT NULL PRIMARY KEY,
"recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE,
"person" TEXT REFERENCES "persons"("id"),
"ensemble" TEXT REFERENCES "ensembles"("id"),
"role" TEXT REFERENCES "instruments"("id")
);
CREATE TABLE "mediums" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"discid" TEXT
);
CREATE TABLE "track_sets" (
"id" TEXT NOT NULL PRIMARY KEY,
"medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE,
"index" INTEGER NOT NULL,
"recording" TEXT NOT NULL REFERENCES "recordings"("id")
);
CREATE TABLE "tracks" (
"id" TEXT NOT NULL PRIMARY KEY,
"track_set" TEXT NOT NULL REFERENCES "track_sets"("id") ON DELETE CASCADE,
"index" INTEGER NOT NULL,
"work_parts" TEXT NOT NULL,
"path" TEXT NOT NULL
);

View file

@ -1 +0,0 @@
de

View file

@ -1,77 +0,0 @@
res/ui/ensemble_editor.ui
res/ui/ensemble_screen.ui
res/ui/ensemble_selector.ui
res/ui/instrument_editor.ui
res/ui/instrument_selector.ui
res/ui/part_editor.ui
res/ui/performance_editor.ui
res/ui/person_editor.ui
res/ui/person_list.ui
res/ui/person_screen.ui
res/ui/person_selector.ui
res/ui/player_bar.ui
res/ui/player_screen.ui
res/ui/poe_list.ui
res/ui/preferences.ui
res/ui/recording_editor.ui
res/ui/recording_screen.ui
res/ui/recording_selector_screen.ui
res/ui/recording_selector.ui
res/ui/section_editor.ui
res/ui/track_editor.ui
res/ui/tracks_editor.ui
res/ui/window.ui
res/ui/work_editor.ui
res/ui/work_screen.ui
res/ui/work_selector.ui
res/ui/work_selector_screen.ui
src/database/database.rs
src/database/models.rs
src/database/mod.rs
src/database/schema.rs
src/database/tables.rs
src/dialogs/about.rs
src/dialogs/ensemble_editor.rs
src/dialogs/ensemble_selector.rs
src/dialogs/instrument_editor.rs
src/dialogs/instrument_selector.rs
src/dialogs/mod.rs
src/dialogs/person_editor.rs
src/dialogs/person_selector.rs
src/dialogs/preferences.rs
src/dialogs/recording/mod.rs
src/dialogs/recording/performance_editor.rs
src/dialogs/recording/recording_dialog.rs
src/dialogs/recording/recording_editor_dialog.rs
src/dialogs/recording/recording_editor.rs
src/dialogs/recording/recording_selector_person_screen.rs
src/dialogs/recording/recording_selector.rs
src/dialogs/recording/recording_selector_work_screen.rs
src/dialogs/track_editor.rs
src/dialogs/tracks_editor.rs
src/dialogs/work/mod.rs
src/dialogs/work/part_editor.rs
src/dialogs/work/section_editor.rs
src/dialogs/work/work_dialog.rs
src/dialogs/work/work_editor_dialog.rs
src/dialogs/work/work_editor.rs
src/dialogs/work/work_selector_person_screen.rs
src/dialogs/work/work_selector.rs
src/screens/ensemble_screen.rs
src/screens/mod.rs
src/screens/person_screen.rs
src/screens/player_screen.rs
src/screens/recording_screen.rs
src/screens/work_screen.rs
src/widgets/list.rs
src/widgets/mod.rs
src/widgets/navigator.rs
src/widgets/person_list.rs
src/widgets/player_bar.rs
src/widgets/poe_list.rs
src/widgets/selector_row.rs
src/backend.rs
src/main.rs
src/player.rs
src/window.rs

View file

@ -1,387 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-08 20:12+0100\n"
"PO-Revision-Date: 2020-11-08 20:13+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.4.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: res/ui/ensemble_editor.ui:20 res/ui/performance_editor.ui:196
#: res/ui/performance_editor.ui:227
msgid "Ensemble"
msgstr "Ensemble"
#: res/ui/ensemble_editor.ui:23 res/ui/instrument_editor.ui:23
#: res/ui/part_editor.ui:25 res/ui/performance_editor.ui:23
#: res/ui/person_editor.ui:23 res/ui/recording_editor.ui:17
#: res/ui/section_editor.ui:23 res/ui/track_editor.ui:24
#: res/ui/tracks_editor.ui:39 res/ui/work_editor.ui:17
msgid "Cancel"
msgstr "Abbrechen"
#: res/ui/ensemble_editor.ui:31 res/ui/instrument_editor.ui:31
#: res/ui/part_editor.ui:33 res/ui/performance_editor.ui:31
#: res/ui/person_editor.ui:31 res/ui/recording_editor.ui:25
#: res/ui/section_editor.ui:31 res/ui/track_editor.ui:32
#: res/ui/tracks_editor.ui:24 res/ui/work_editor.ui:25
msgid "Save"
msgstr "Speichern"
#: res/ui/ensemble_editor.ui:64 res/ui/instrument_editor.ui:64
msgid "Name"
msgstr "Name"
#: res/ui/ensemble_screen.ui:90 res/ui/work_screen.ui:90
msgid "Search recordings …"
msgstr "Aufnahmen durchsuchen …"
#: res/ui/ensemble_screen.ui:145 res/ui/person_screen.ui:196
#: res/ui/work_screen.ui:145
msgid "Recordings"
msgstr "Aufnahmen"
#: res/ui/ensemble_screen.ui:188 res/ui/work_screen.ui:188
#: src/dialogs/recording/recording_selector_work_screen.rs:38
#: src/screens/ensemble_screen.rs:53 src/screens/person_screen.rs:77
#: src/screens/work_screen.rs:54
msgid "No recordings found."
msgstr "Keine Aufnahmen gefunden."
#: res/ui/ensemble_selector.ui:22
msgid "Select ensemble"
msgstr "Ensemble auswählen"
#: res/ui/ensemble_selector.ui:84
msgid "No ensembles found."
msgstr "Keine Ensembles gefunden."
#: res/ui/instrument_editor.ui:20
msgid "Instrument"
msgstr "Instrument"
#: res/ui/instrument_selector.ui:22
msgid "Select instrument"
msgstr "Instrument auswählen"
#: res/ui/instrument_selector.ui:84
msgid "No instruments found."
msgstr "Keine Instrumente gefunden."
#: res/ui/part_editor.ui:22
msgid "Work part"
msgstr "Werkabschnitt"
#: res/ui/part_editor.ui:70 res/ui/work_editor.ui:84
msgid "Composer"
msgstr "Komponist"
#: res/ui/part_editor.ui:93 res/ui/player_bar.ui:118
#: res/ui/player_screen.ui:160 res/ui/section_editor.ui:64
#: res/ui/work_editor.ui:106
msgid "Title"
msgstr "Titel"
#: res/ui/part_editor.ui:116 res/ui/performance_editor.ui:87
#: res/ui/performance_editor.ui:170 res/ui/performance_editor.ui:214
#: res/ui/recording_editor.ui:81 res/ui/tracks_editor.ui:93
#: res/ui/work_editor.ui:69 src/dialogs/recording/performance_editor.rs:150
#: src/dialogs/recording/performance_editor.rs:160
#: src/dialogs/recording/performance_editor.rs:170
#: src/dialogs/work/part_editor.rs:166
msgid "Select …"
msgstr "Auswählen …"
#: res/ui/part_editor.ui:159 res/ui/recording_editor.ui:119
#: res/ui/work_editor.ui:119
msgid "Overview"
msgstr "Überblick"
#: res/ui/part_editor.ui:247 res/ui/work_editor.ui:207
msgid "Instruments"
msgstr "Instrumente"
#: res/ui/performance_editor.ui:20
msgid "Performance"
msgstr "Auftritt"
#: res/ui/performance_editor.ui:64
msgid "Role"
msgstr "Rolle"
#: res/ui/performance_editor.ui:128
msgid "Type"
msgstr "Typ"
#: res/ui/performance_editor.ui:151 res/ui/performance_editor.ui:183
#: res/ui/person_editor.ui:20
msgid "Person"
msgstr "Person"
#: res/ui/person_editor.ui:64
msgid "First name"
msgstr "Vorname"
#: res/ui/person_editor.ui:87
msgid "Last name"
msgstr "Nachname"
#: res/ui/person_list.ui:28
msgid "Search persons …"
msgstr "Personen durchsuchen …"
#: res/ui/person_screen.ui:90
msgid "Search works and recordings …"
msgstr "Werke und Aufnahmen durchsuchen …"
#: res/ui/person_screen.ui:151
msgid "Works"
msgstr "Werke"
#: res/ui/person_screen.ui:246
msgid "No works or recordings found."
msgstr "Keine Werke oder Aufnahmen gefunden."
#: res/ui/person_selector.ui:22
msgid "Select person"
msgstr "Person auswählen"
#: res/ui/player_bar.ui:135 res/ui/player_screen.ui:177
msgid "Subtitle"
msgstr "Untertitel"
#: res/ui/player_bar.ui:181 res/ui/player_bar.ui:205
#: res/ui/player_screen.ui:229 res/ui/player_screen.ui:257
msgid "0:00"
msgstr "0:00"
#: res/ui/player_bar.ui:193
msgid "/"
msgstr "/"
#: res/ui/player_screen.ui:24
msgid "Player"
msgstr "Wiedergabe"
#: res/ui/poe_list.ui:28
msgid "Search persons and ensembles …"
msgstr "Personen und Ensembles durchsuchen …"
#: res/ui/preferences.ui:16
msgid "General"
msgstr "Allgemein"
#: res/ui/preferences.ui:21
msgid "Music library"
msgstr "Musikbibliothek"
#: res/ui/preferences.ui:27
msgid "Music library folder"
msgstr "Ordner der Musikbibliothek"
#: res/ui/preferences.ui:29
msgid "None selected"
msgstr "Keiner ausgewählt"
#: res/ui/preferences.ui:32
msgid "Select"
msgstr "Auswählen"
#: res/ui/recording_editor.ui:14 res/ui/tracks_editor.ui:68
msgid "Recording"
msgstr "Aufnahme"
#: res/ui/recording_editor.ui:63
msgid "Comment"
msgstr "Kommentar"
#: res/ui/recording_editor.ui:106 res/ui/tracks_editor.ui:109
#: res/ui/work_editor.ui:14
msgid "Work"
msgstr "Werk"
#: res/ui/recording_editor.ui:226 res/ui/tracks_editor.ui:127
msgid "Performers"
msgstr "Interpreten"
#: res/ui/recording_screen.ui:98 res/ui/tracks_editor.ui:21
msgid "Tracks"
msgstr "Tracks"
#: res/ui/recording_screen.ui:127
msgid "Add to playlist"
msgstr "Zur Wiedergabeliste hinzufügen"
#: res/ui/recording_selector.ui:27 res/ui/work_selector.ui:26
msgid "Select a composer on the left."
msgstr "Wählen Sie einen Komponisten aus."
#: res/ui/recording_selector.ui:51
msgid "Select a recording"
msgstr "Aufnahme auswählen"
#: res/ui/section_editor.ui:20
msgid "Work section"
msgstr "Werkteil"
#: res/ui/track_editor.ui:21
msgid "Track"
msgstr "Track"
#: res/ui/track_editor.ui:70
msgid "Select a recording of a work with multiple parts."
msgstr "Wählen Sie eine Aufnahme eines mehrteiligen Werks aus."
#: res/ui/window.ui:51 res/ui/window.ui:141
msgid "Welcome to Musicus!"
msgstr "Willkommen bei Musicus!"
#: res/ui/window.ui:67
msgid ""
"Get startet by selecting something from the sidebar or adding new things to "
"your library using the button in the top left corner."
msgstr ""
"Legen Sie los, indem Sie etwas in der Seitenleiste auswählen oder fügen Sie "
"mit dem Knopf oben links neue Aufnahmen zu Ihrer Musikbibliothek hinzu."
#: res/ui/window.ui:104 res/ui/window.ui:252 src/dialogs/about.rs:10
msgid "Musicus"
msgstr "Musicus"
#: res/ui/window.ui:157
msgid ""
"Get startet by selecting the folder containing your music files! Musicus "
"will create a new database there or open one that already exists."
msgstr ""
"Wählen Sie als Erstes den Ordner aus, worin sich Ihre Musik befindet. "
"Musicus wird dort eine neue Datenbank anlegen oder eine bereits existierende "
"öffnen."
#: res/ui/window.ui:170
msgid "Select folder"
msgstr "Ordner auswählen"
#: res/ui/window.ui:331
msgid "Preferences"
msgstr "Einstellungen"
#: res/ui/window.ui:335
msgid "About Musicus"
msgstr "Über Musicus"
#: res/ui/work_editor.ui:373
msgid "Structure"
msgstr "Struktur"
#: res/ui/work_selector.ui:51
msgid "Select a work"
msgstr "Werk auswählen"
#: src/dialogs/about.rs:12
msgid "The classical music player and organizer."
msgstr "Das Programm zum Abspielen und Organisieren von Klassik."
#: src/dialogs/about.rs:14
msgid "Further information and source code"
msgstr "Weitere Informationen und Quellcode"
#: src/dialogs/preferences.rs:30 src/window.rs:70
msgid "Select music library folder"
msgstr "Ordner der Musikbibliothek auswählen"
#: src/dialogs/recording/recording_editor.rs:54
msgid "No performers added."
msgstr "Keine Interpreten hinzugefügt."
#: src/dialogs/recording/recording_selector_person_screen.rs:39
#: src/dialogs/work/work_selector_person_screen.rs:36
#: src/screens/person_screen.rs:57
msgid "No works found."
msgstr "Keine Werke gefunden."
#: src/dialogs/tracks_editor.rs:60
msgid "Add some tracks."
msgstr "Fügen Sie Tracks hinzu."
#: src/dialogs/tracks_editor.rs:118
msgid "Select audio files"
msgstr "Audiodateien auswählen"
#: src/dialogs/tracks_editor.rs:236 src/screens/player_screen.rs:230
#: src/screens/recording_screen.rs:79
msgid "Unknown"
msgstr "Unbekannt"
#: src/dialogs/work/part_editor.rs:49 src/dialogs/work/work_editor.rs:69
msgid "No instruments added."
msgstr "Keine Instrumente hinzugefügt."
#: src/dialogs/work/work_editor.rs:72
msgid "No work parts added."
msgstr "Keine Werkabschnitte hinzugefügt."
#: src/screens/ensemble_screen.rs:35
msgid "Edit ensemble"
msgstr "Ensemble bearbeiten"
#: src/screens/ensemble_screen.rs:41
msgid "Delete ensemble"
msgstr "Ensemble löschen"
#: src/screens/person_screen.rs:39
msgid "Edit person"
msgstr "Person bearbeiten"
#: src/screens/person_screen.rs:45
msgid "Delete person"
msgstr "Person löschen"
#: src/screens/recording_screen.rs:36
msgid "Edit recording"
msgstr "Aufnahme bearbeiten"
#: src/screens/recording_screen.rs:42
msgid "Delete recording"
msgstr "Aufnahme löschen"
#: src/screens/recording_screen.rs:48
msgid "Edit tracks"
msgstr "Tracks bearbeiten"
#: src/screens/recording_screen.rs:54
msgid "Delete tracks"
msgstr "Tracks löschen"
#: src/screens/recording_screen.rs:69
msgid "No tracks found."
msgstr "Keine Tracks gefunden."
#: src/screens/work_screen.rs:36
msgid "Edit work"
msgstr "Werk bearbeiten"
#: src/screens/work_screen.rs:42
msgid "Delete work"
msgstr "Werk löschen"
#: src/widgets/person_list.rs:26
msgid "No persons found."
msgstr "Keine Personen gefunden."
#: src/widgets/poe_list.rs:41
msgid "No persons or ensembles found."
msgstr "Keine Personen oder Ensembles gefunden."
#~ msgid "Search works …"
#~ msgstr "Werke durchsuchen …"

View file

@ -1 +0,0 @@
i18n.gettext('musicus', preset: 'glib')

View file

@ -1,378 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the musicus package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: musicus\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-08 20:12+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: res/ui/ensemble_editor.ui:20 res/ui/performance_editor.ui:196
#: res/ui/performance_editor.ui:227
msgid "Ensemble"
msgstr ""
#: res/ui/ensemble_editor.ui:23 res/ui/instrument_editor.ui:23
#: res/ui/part_editor.ui:25 res/ui/performance_editor.ui:23
#: res/ui/person_editor.ui:23 res/ui/recording_editor.ui:17
#: res/ui/section_editor.ui:23 res/ui/track_editor.ui:24
#: res/ui/tracks_editor.ui:39 res/ui/work_editor.ui:17
msgid "Cancel"
msgstr ""
#: res/ui/ensemble_editor.ui:31 res/ui/instrument_editor.ui:31
#: res/ui/part_editor.ui:33 res/ui/performance_editor.ui:31
#: res/ui/person_editor.ui:31 res/ui/recording_editor.ui:25
#: res/ui/section_editor.ui:31 res/ui/track_editor.ui:32
#: res/ui/tracks_editor.ui:24 res/ui/work_editor.ui:25
msgid "Save"
msgstr ""
#: res/ui/ensemble_editor.ui:64 res/ui/instrument_editor.ui:64
msgid "Name"
msgstr ""
#: res/ui/ensemble_screen.ui:90 res/ui/work_screen.ui:90
msgid "Search recordings …"
msgstr ""
#: res/ui/ensemble_screen.ui:145 res/ui/person_screen.ui:196
#: res/ui/work_screen.ui:145
msgid "Recordings"
msgstr ""
#: res/ui/ensemble_screen.ui:188 res/ui/work_screen.ui:188
#: src/dialogs/recording/recording_selector_work_screen.rs:38
#: src/screens/ensemble_screen.rs:53 src/screens/person_screen.rs:77
#: src/screens/work_screen.rs:54
msgid "No recordings found."
msgstr ""
#: res/ui/ensemble_selector.ui:22
msgid "Select ensemble"
msgstr ""
#: res/ui/ensemble_selector.ui:84
msgid "No ensembles found."
msgstr ""
#: res/ui/instrument_editor.ui:20
msgid "Instrument"
msgstr ""
#: res/ui/instrument_selector.ui:22
msgid "Select instrument"
msgstr ""
#: res/ui/instrument_selector.ui:84
msgid "No instruments found."
msgstr ""
#: res/ui/part_editor.ui:22
msgid "Work part"
msgstr ""
#: res/ui/part_editor.ui:70 res/ui/work_editor.ui:84
msgid "Composer"
msgstr ""
#: res/ui/part_editor.ui:93 res/ui/player_bar.ui:118
#: res/ui/player_screen.ui:160 res/ui/section_editor.ui:64
#: res/ui/work_editor.ui:106
msgid "Title"
msgstr ""
#: res/ui/part_editor.ui:116 res/ui/performance_editor.ui:87
#: res/ui/performance_editor.ui:170 res/ui/performance_editor.ui:214
#: res/ui/recording_editor.ui:81 res/ui/tracks_editor.ui:93
#: res/ui/work_editor.ui:69 src/dialogs/recording/performance_editor.rs:150
#: src/dialogs/recording/performance_editor.rs:160
#: src/dialogs/recording/performance_editor.rs:170
#: src/dialogs/work/part_editor.rs:166
msgid "Select …"
msgstr ""
#: res/ui/part_editor.ui:159 res/ui/recording_editor.ui:119
#: res/ui/work_editor.ui:119
msgid "Overview"
msgstr ""
#: res/ui/part_editor.ui:247 res/ui/work_editor.ui:207
msgid "Instruments"
msgstr ""
#: res/ui/performance_editor.ui:20
msgid "Performance"
msgstr ""
#: res/ui/performance_editor.ui:64
msgid "Role"
msgstr ""
#: res/ui/performance_editor.ui:128
msgid "Type"
msgstr ""
#: res/ui/performance_editor.ui:151 res/ui/performance_editor.ui:183
#: res/ui/person_editor.ui:20
msgid "Person"
msgstr ""
#: res/ui/person_editor.ui:64
msgid "First name"
msgstr ""
#: res/ui/person_editor.ui:87
msgid "Last name"
msgstr ""
#: res/ui/person_list.ui:28
msgid "Search persons …"
msgstr ""
#: res/ui/person_screen.ui:90
msgid "Search works and recordings …"
msgstr ""
#: res/ui/person_screen.ui:151
msgid "Works"
msgstr ""
#: res/ui/person_screen.ui:246
msgid "No works or recordings found."
msgstr ""
#: res/ui/person_selector.ui:22
msgid "Select person"
msgstr ""
#: res/ui/player_bar.ui:135 res/ui/player_screen.ui:177
msgid "Subtitle"
msgstr ""
#: res/ui/player_bar.ui:181 res/ui/player_bar.ui:205
#: res/ui/player_screen.ui:229 res/ui/player_screen.ui:257
msgid "0:00"
msgstr ""
#: res/ui/player_bar.ui:193
msgid "/"
msgstr ""
#: res/ui/player_screen.ui:24
msgid "Player"
msgstr ""
#: res/ui/poe_list.ui:28
msgid "Search persons and ensembles …"
msgstr ""
#: res/ui/preferences.ui:16
msgid "General"
msgstr ""
#: res/ui/preferences.ui:21
msgid "Music library"
msgstr ""
#: res/ui/preferences.ui:27
msgid "Music library folder"
msgstr ""
#: res/ui/preferences.ui:29
msgid "None selected"
msgstr ""
#: res/ui/preferences.ui:32
msgid "Select"
msgstr ""
#: res/ui/recording_editor.ui:14 res/ui/tracks_editor.ui:68
msgid "Recording"
msgstr ""
#: res/ui/recording_editor.ui:63
msgid "Comment"
msgstr ""
#: res/ui/recording_editor.ui:106 res/ui/tracks_editor.ui:109
#: res/ui/work_editor.ui:14
msgid "Work"
msgstr ""
#: res/ui/recording_editor.ui:226 res/ui/tracks_editor.ui:127
msgid "Performers"
msgstr ""
#: res/ui/recording_screen.ui:98 res/ui/tracks_editor.ui:21
msgid "Tracks"
msgstr ""
#: res/ui/recording_screen.ui:127
msgid "Add to playlist"
msgstr ""
#: res/ui/recording_selector.ui:27 res/ui/work_selector.ui:26
msgid "Select a composer on the left."
msgstr ""
#: res/ui/recording_selector.ui:51
msgid "Select a recording"
msgstr ""
#: res/ui/section_editor.ui:20
msgid "Work section"
msgstr ""
#: res/ui/track_editor.ui:21
msgid "Track"
msgstr ""
#: res/ui/track_editor.ui:70
msgid "Select a recording of a work with multiple parts."
msgstr ""
#: res/ui/window.ui:51 res/ui/window.ui:141
msgid "Welcome to Musicus!"
msgstr ""
#: res/ui/window.ui:67
msgid ""
"Get startet by selecting something from the sidebar or adding new things to "
"your library using the button in the top left corner."
msgstr ""
#: res/ui/window.ui:104 res/ui/window.ui:252 src/dialogs/about.rs:10
msgid "Musicus"
msgstr ""
#: res/ui/window.ui:157
msgid ""
"Get startet by selecting the folder containing your music files! Musicus "
"will create a new database there or open one that already exists."
msgstr ""
#: res/ui/window.ui:170
msgid "Select folder"
msgstr ""
#: res/ui/window.ui:331
msgid "Preferences"
msgstr ""
#: res/ui/window.ui:335
msgid "About Musicus"
msgstr ""
#: res/ui/work_editor.ui:373
msgid "Structure"
msgstr ""
#: res/ui/work_selector.ui:51
msgid "Select a work"
msgstr ""
#: src/dialogs/about.rs:12
msgid "The classical music player and organizer."
msgstr ""
#: src/dialogs/about.rs:14
msgid "Further information and source code"
msgstr ""
#: src/dialogs/preferences.rs:30 src/window.rs:70
msgid "Select music library folder"
msgstr ""
#: src/dialogs/recording/recording_editor.rs:54
msgid "No performers added."
msgstr ""
#: src/dialogs/recording/recording_selector_person_screen.rs:39
#: src/dialogs/work/work_selector_person_screen.rs:36
#: src/screens/person_screen.rs:57
msgid "No works found."
msgstr ""
#: src/dialogs/tracks_editor.rs:60
msgid "Add some tracks."
msgstr ""
#: src/dialogs/tracks_editor.rs:118
msgid "Select audio files"
msgstr ""
#: src/dialogs/tracks_editor.rs:236 src/screens/player_screen.rs:230
#: src/screens/recording_screen.rs:79
msgid "Unknown"
msgstr ""
#: src/dialogs/work/part_editor.rs:49 src/dialogs/work/work_editor.rs:69
msgid "No instruments added."
msgstr ""
#: src/dialogs/work/work_editor.rs:72
msgid "No work parts added."
msgstr ""
#: src/screens/ensemble_screen.rs:35
msgid "Edit ensemble"
msgstr ""
#: src/screens/ensemble_screen.rs:41
msgid "Delete ensemble"
msgstr ""
#: src/screens/person_screen.rs:39
msgid "Edit person"
msgstr ""
#: src/screens/person_screen.rs:45
msgid "Delete person"
msgstr ""
#: src/screens/recording_screen.rs:36
msgid "Edit recording"
msgstr ""
#: src/screens/recording_screen.rs:42
msgid "Delete recording"
msgstr ""
#: src/screens/recording_screen.rs:48
msgid "Edit tracks"
msgstr ""
#: src/screens/recording_screen.rs:54
msgid "Delete tracks"
msgstr ""
#: src/screens/recording_screen.rs:69
msgid "No tracks found."
msgstr ""
#: src/screens/work_screen.rs:36
msgid "Edit work"
msgstr ""
#: src/screens/work_screen.rs:42
msgid "Delete work"
msgstr ""
#: src/widgets/person_list.rs:26
msgid "No persons found."
msgstr ""
#: src/widgets/poe_list.rs:41
msgid "No persons or ensembles found."
msgstr ""

View file

@ -1,9 +0,0 @@
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
gnome = import('gnome')
resources = gnome.compile_resources('musicus',
'musicus.gresource.xml',
gresource_bundle: true,
install: true,
install_dir: pkgdatadir,
)

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/de/johrpan/musicus">
<file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_selector.ui</file>
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
<file preprocess="xml-stripblanks">ui/instrument_selector.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/performance_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_list.ui</file>
<file preprocess="xml-stripblanks">ui/person_screen.ui</file>
<file preprocess="xml-stripblanks">ui/person_selector.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/poe_list.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_screen.ui</file>
<file preprocess="xml-stripblanks">ui/recording_selector.ui</file>
<file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file>
<file preprocess="xml-stripblanks">ui/selector.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/track_editor.ui</file>
<file preprocess="xml-stripblanks">ui/track_selector.ui</file>
<file preprocess="xml-stripblanks">ui/track_set_editor.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_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_selector.ui</file>
<file preprocess="xml-stripblanks">ui/work_selector_screen.ui</file>
</gresource>
</gresources>

View file

@ -1,189 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Ensemble</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="revealed">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Name</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="name_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Publish</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="upload_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="active">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Ensemble</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,216 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar" id="header">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="menu-model">menu</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-more-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="search_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">400</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search recordings …</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<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="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="recording_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">No recordings found.</property>
</object>
<packing>
<property name="name">nothing</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</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,210 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="0.0"/>
<object class="HdyWindow" id="window">
<property name="can-focus">False</property>
<property name="modal">True</property>
<property name="destroy-with-parent">True</property>
<property name="type-hint">dialog</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Select ensemble</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search ensembles …</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="server_check_button">
<property name="label" translatable="yes">Show ensembles from the Musicus server</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,189 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Instrument</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="revealed">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Name</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="name_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Publish</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="upload_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="active">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Instrument</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,210 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="0.0"/>
<object class="HdyWindow" id="window">
<property name="can-focus">False</property>
<property name="modal">True</property>
<property name="destroy-with-parent">True</property>
<property name="type-hint">dialog</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Select instrument</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search instruments …</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="server_check_button">
<property name="label" translatable="yes">Show instruments from the Musicus server</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,219 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="0.0"/>
<object class="HdyWindow" id="window">
<property name="can-focus">False</property>
<property name="modal">True</property>
<property name="default-width">350</property>
<property name="type-hint">dialog</property>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Login</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="login_button">
<property name="label" translatable="yes">Login</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="has-default">True</property>
<property name="receives-default">False</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="message-type">error</property>
<property name="revealed">False</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout-style">end</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">The login credentials were wrong!</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Username</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Password</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="username_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="visibility">False</property>
<property name="activates-default">True</property>
<property name="input-purpose">password</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Login</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,201 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="visible">True</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Import music</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="done_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="sensitive">False</property>
<child>
<object class="GtkStack" id="done_stack">
<property name="visible">True</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="active">True</property>
</object>
</child>
<child>
<object class="GtkImage" id="done">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="revealed">False</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="vexpand">True</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<property name="label" translatable="yes">Medium</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="HdyActionRow" id="name_row">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="activatable">True</property>
<property name="title" translatable="yes">Name of the medium</property>
<property name="activatable-widget">name_entry</property>
<child>
<object class="GtkEntry" id="name_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="HdyActionRow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="activatable">True</property>
<property name="title" translatable="yes">Publish to the server</property>
<property name="activatable-widget">publish_switch</property>
<child>
<object class="GtkSwitch" id="publish_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
<property name="active">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">horizontal</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="valign">end</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkFrame" id="frame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,281 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Performance</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=3 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Role</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkButton" id="role_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkLabel" id="role_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="reset_role_button">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Type</property>
<property name="justify">right</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Person</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="person_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkLabel" id="person_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">person</property>
<property name="title" translatable="yes">Person</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Ensemble</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ensemble_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkLabel" id="ensemble_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">ensemble</property>
<property name="title" translatable="yes">Ensemble</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkStackSwitcher">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="homogeneous">True</property>
<property name="stack">stack</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="GtkSizeGroup">
<widgets>
<widget name="label1"/>
<widget name="label2"/>
<widget name="label3"/>
<widget name="label4"/>
</widgets>
</object>
</interface>

View file

@ -1,212 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Person</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="revealed">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=3 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">First name</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="first_name_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Last name</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="last_name_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Publish</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="upload_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="active">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Person</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,75 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">400</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search persons …</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,274 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar" id="header">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="menu-model">menu</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-more-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="search_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">400</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search works and recordings …</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<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="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkBox" id="work_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Works</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="work_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="recording_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="recording_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">No works or recordings found.</property>
</object>
<packing>
<property name="name">nothing</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</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,210 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="0.0"/>
<object class="HdyWindow" id="window">
<property name="can-focus">False</property>
<property name="modal">True</property>
<property name="destroy-with-parent">True</property>
<property name="type-hint">dialog</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Select person</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search persons …</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="server_check_button">
<property name="label" translatable="yes">Show persons from the Musicus server</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,230 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkImage" id="play_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-start-symbolic</property>
</object>
<object class="GtkRevealer" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">slide-up</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">6</property>
<property name="spacing">12</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="previous_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-skip-backward-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="play_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage" id="pause_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-pause-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="next_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-skip-forward-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Title</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="subtitle_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Subtitle</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="playlist_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-list-bullet-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="position_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0:00</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">/</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="duration_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0:00</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,305 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkImage" id="play_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-start-symbolic</property>
</object>
<object class="GtkAdjustment" id="position">
<property name="upper">1</property>
<property name="step-increment">0.01</property>
<property name="page-increment">0.05</property>
</object>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Player</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">12</property>
<property name="margin-right">12</property>
<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="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">12</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="previous_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-skip-backward-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="play_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage" id="pause_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-pause-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="next_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-skip-forward-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Title</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="subtitle_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Subtitle</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="stop_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-stop-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="position_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0:00</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="position_scale">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="events">GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="adjustment">position</property>
<property name="upper-stepper-sensitivity">off</property>
<property name="round-digits">1</property>
<property name="draw-value">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="duration_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0:00</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,75 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">400</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search persons and ensembles …</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="0.0"/>
<object class="HdyPreferencesWindow" id="window">
<property name="can-focus">False</property>
<property name="modal">True</property>
<property name="default-width">400</property>
<property name="default-height">400</property>
<property name="type-hint">dialog</property>
<child>
<object class="HdyPreferencesPage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">General</property>
<child>
<object class="HdyPreferencesGroup">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Music library</property>
<child>
<object class="HdyActionRow" id="music_library_path_row">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="selectable">False</property>
<property name="title" translatable="yes">Music library folder</property>
<property name="activatable-widget">select_music_library_path_button</property>
<property name="subtitle" translatable="yes">None selected</property>
<child>
<object class="GtkButton" id="select_music_library_path_button">
<property name="label" translatable="yes">Select</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="HdyPreferencesGroup">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Server connection</property>
<child>
<object class="HdyActionRow" id="url_row">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="selectable">False</property>
<property name="title" translatable="yes">Server URL</property>
<property name="activatable-widget">url_button</property>
<property name="subtitle" translatable="yes">Not set</property>
<child>
<object class="GtkButton" id="url_button">
<property name="label" translatable="yes">Change</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="HdyActionRow" id="login_row">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="selectable">False</property>
<property name="title" translatable="yes">Login credentials</property>
<property name="activatable-widget">login_button</property>
<property name="subtitle" translatable="yes">Not logged in</property>
<child>
<object class="GtkButton" id="login_button">
<property name="label" translatable="yes">Change</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<object class="GtkSizeGroup">
<widgets>
<widget name="select_music_library_path_button"/>
<widget name="url_button"/>
<widget name="login_button"/>
</widgets>
</object>
</interface>

View file

@ -1,373 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Recording</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="revealed">False</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkNotebook">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="show-border">False</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=3 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Comment</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="work_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel" id="work_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
<property name="ellipsize">end</property>
</object>
</child>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="comment_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Work</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Publish</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="upload_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="active">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Overview</property>
</object>
<packing>
<property name="tab-fill">False</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">0</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="add_performer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="edit_performer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_performer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-remove-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Performers</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab-fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Recording</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,185 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar" id="header">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="menu-model">menu</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-more-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<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="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Tracks</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_to_playlist_button">
<property name="label" translatable="yes">Add to playlist</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">end</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</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>

View file

@ -1,258 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="empty_screen">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="show-close-button">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Select a composer on the left.</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="HdyLeaflet" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="visible-child">sidebar_box</property>
<child>
<object class="GtkBox" id="sidebar_box">
<property name="width-request">250</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Select a recording</property>
<property name="show-close-button" bind-source="widget" bind-property="folded" bind-flags="sync-create">False</property>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search composers …</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="server_check_button">
<property name="label" translatable="yes">Show recordings from the server</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hhomogeneous">False</property>
<property name="transition-type">crossfade</property>
<property name="interpolate-size">True</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">sidebar</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<style>
<class name="sidebar"/>
</style>
</object>
<packing>
<property name="navigatable">False</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="0.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar" id="header">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hhomogeneous">False</property>
<property name="transition-type">crossfade</property>
<property name="interpolate-size">True</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,264 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="width-request">250</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar" id="header">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search …</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="server_check_button">
<property name="label" translatable="yes">Use the Musicus server</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="halign">start</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hhomogeneous">False</property>
<property name="vhomogeneous">False</property>
<property name="transition-type">crossfade</property>
<property name="interpolate-size">True</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">12</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="height-request">200</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkFrame" id="frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">start</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="0.0"/>
<object class="HdyWindow" id="window">
<property name="can-focus">False</property>
<property name="modal">True</property>
<property name="destroy-with-parent">True</property>
<property name="type-hint">dialog</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Server</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="set_button">
<property name="label" translatable="yes">Set</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="has-default">True</property>
<property name="receives-default">True</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<!-- n-columns=2 n-rows=1 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">URL</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="url_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="hexpand">True</property>
<property name="activates-default">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,161 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Import music</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="message-type">error</property>
<property name="revealed">False</property>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Failed to load the CD. Make sure you have inserted it into your drive.</property>
<property name="wrap">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">media-optical-cd-audio-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">Import from audio CD</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">Insert an audio compact disc into your drive and click the button below. The disc will be copied in the background while you set up the metadata.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="import_button">
<property name="label" translatable="yes">Import</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">start</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Track</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="select_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<style>
<class name="suggested-action"/>
</style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<child>
<object class="GtkFrame" id="parts_frame">
<property name="visible">True</property>
<property name="valign">start</property>
<property name="margin-top">12</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-bottom">6</property>
<property name="shadow-type">in</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,75 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Select tracks</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="select_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="sensitive">False</property>
<style>
<class name="suggested-action"/>
</style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<child>
<object class="GtkFrame" id="tracks_frame">
<property name="visible">True</property>
<property name="valign">start</property>
<property name="margin-top">12</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-bottom">6</property>
<property name="shadow-type">in</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Tracks</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="sensitive">False</property>
<style>
<class name="suggested-action"/>
</style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<property name="label" translatable="yes">Recording</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="HdyActionRow" id="recording_row">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="activatable">True</property>
<property name="title" translatable="yes">Select a recording</property>
<property name="activatable-widget">select_recording_button</property>
<child>
<object class="GtkButton" id="select_recording_button">
<property name="label" translatable="yes">Select</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">horizontal</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="valign">end</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Tracks</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkButton" id="edit_tracks_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">document-edit-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkFrame" id="tracks_frame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,344 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22" />
<requires lib="libhandy" version="1.0" />
<object class="GtkBox" id="empty_screen">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="show-close-button">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5</property>
<property name="pixel-size">80</property>
<property name="icon-name">folder-music-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5</property>
<property name="label" translatable="yes">Welcome to Musicus!</property>
<attributes>
<attribute name="size" value="16384" />
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5</property>
<property name="label" translatable="yes">Get startet by selecting something from the sidebar or adding new things to your library using the button in the top left corner.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="HdyApplicationWindow" id="window">
<property name="can-focus">False</property>
<property name="default-width">800</property>
<property name="default-height">566</property>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Musicus</property>
<property name="show-close-button">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">folder-music-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">Welcome to Musicus!</property>
<attributes>
<attribute name="size" value="16384" />
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">Get startet by selecting the folder containing your music files! Musicus will create a new database there or open one that already exists.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="select_music_library_path_button">
<property name="label" translatable="yes">Select folder</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action" />
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">empty</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="content_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyLeaflet" id="leaflet">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="sidebar_box">
<property name="width-request">250</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Musicus</property>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="menu-model">menu</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">open-menu-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">sidebar</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<style>
<class name="sidebar" />
</style>
</object>
<packing>
<property name="navigatable">False</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">Import CD</attribute>
<attribute name="action">win.import-disc</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Preferences</attribute>
<attribute name="action">win.preferences</attribute>
</item>
<item>
<attribute name="label" translatable="yes">About Musicus</attribute>
<attribute name="action">win.about</attribute>
</item>
</section>
</menu>
</interface>

View file

@ -1,529 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Work</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="revealed">False</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkNotebook">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="show-border">False</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=3 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkButton" id="composer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel" id="composer_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
<property name="ellipsize">end</property>
</object>
</child>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Composer</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="title_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="composer_labe">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Title</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Publish</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="upload_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="active">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Overview</property>
</object>
<packing>
<property name="tab-fill">False</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">800</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow" id="instruments_scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">0</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="add_instrument_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_instrument_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-remove-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Instruments</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab-fill">False</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">800</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow" id="structure_scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="add_part_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_section_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">folder-new-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="edit_part_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_part_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-remove-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="move_part_down_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-down-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="move_part_up_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-up-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="position">2</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Structure</property>
</object>
<packing>
<property name="position">2</property>
<property name="tab-fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Work</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,169 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Work part</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Composer</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="title_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="composer_labe">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Title</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkButton" id="composer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel" id="composer_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="reset_composer_button">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,216 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar" id="header">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="menu-model">menu</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-more-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="search_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">400</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search recordings …</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<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="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="recording_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">No recordings found.</property>
</object>
<packing>
<property name="name">nothing</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</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

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Work section</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=1 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Title</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="title_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,275 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="empty_screen">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="show-close-button">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Select a composer on the left.</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="HdyLeaflet" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="visible-child">sidebar_box</property>
<child>
<object class="GtkBox" id="sidebar_box">
<property name="width-request">250</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Select a work</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdySearchBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search composers …</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="server_check_button">
<property name="label" translatable="yes">Show works from the server</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hhomogeneous">False</property>
<property name="transition-type">crossfade</property>
<property name="interpolate-size">True</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">sidebar</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<style>
<class name="sidebar"/>
</style>
</object>
<packing>
<property name="navigatable">False</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="0.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar" id="header">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hhomogeneous">False</property>
<property name="transition-type">crossfade</property>
<property name="interpolate-size">True</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">network-error-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">An error occured!</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="try_again_button">
<property name="label" translatable="yes">Try again</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">error</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -1,18 +0,0 @@
use super::Backend;
use crate::database::Ensemble;
use anyhow::Result;
impl Backend {
/// Get all available ensembles from the server.
pub async fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
let body = self.get("ensembles").await?;
let ensembles: Vec<Ensemble> = serde_json::from_str(&body)?;
Ok(ensembles)
}
/// Post a new ensemble to the server.
pub async fn post_ensemble(&self, data: &Ensemble) -> Result<()> {
self.post("ensembles", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,18 +0,0 @@
use super::Backend;
use crate::database::Instrument;
use anyhow::Result;
impl Backend {
/// Get all available instruments from the server.
pub async fn get_instruments(&self) -> Result<Vec<Instrument>> {
let body = self.get("instruments").await?;
let instruments: Vec<Instrument> = serde_json::from_str(&body)?;
Ok(instruments)
}
/// Post a new instrument to the server.
pub async fn post_instrument(&self, data: &Instrument) -> Result<()> {
self.post("instruments", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,170 +0,0 @@
use super::secure;
use super::Backend;
use anyhow::{anyhow, bail, Result};
use gio::prelude::*;
use isahc::http::StatusCode;
use isahc::prelude::*;
use serde::Serialize;
use std::time::Duration;
pub mod ensembles;
pub use ensembles::*;
pub mod instruments;
pub use instruments::*;
pub mod persons;
pub use persons::*;
pub mod recordings;
pub use recordings::*;
pub mod works;
pub use works::*;
/// Credentials used for login.
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LoginData {
pub username: String,
pub password: String,
}
impl Backend {
/// Initialize the client.
pub(super) fn init_client(&self) -> Result<()> {
if let Some(data) = secure::load_login_data()? {
self.login_data.replace(Some(data));
}
if let Some(url) = self.settings.get_string("server-url") {
if !url.is_empty() {
self.server_url.replace(Some(url.to_string()));
}
}
Ok(())
}
/// Set the URL of the Musicus server to connect to.
pub fn set_server_url(&self, url: &str) -> Result<()> {
self.settings.set_string("server-url", url)?;
self.server_url.replace(Some(url.to_string()));
Ok(())
}
/// Get the currently used login token.
pub fn get_token(&self) -> Option<String> {
self.token.borrow().clone()
}
/// Set the login token to use. This will be done automatically by the login method.
pub fn set_token(&self, token: &str) {
self.token.replace(Some(token.to_string()));
}
/// Get the currently set server URL.
pub fn get_server_url(&self) -> Option<String> {
self.server_url.borrow().clone()
}
/// Get the currently stored login credentials.
pub fn get_login_data(&self) -> Option<LoginData> {
self.login_data.borrow().clone()
}
/// Set the user credentials to use.
pub async fn set_login_data(&self, data: LoginData) -> Result<()> {
secure::store_login_data(data.clone()).await?;
self.login_data.replace(Some(data));
self.token.replace(None);
Ok(())
}
/// Try to login a user with the provided credentials and return, wether the login suceeded.
pub async fn login(&self) -> Result<bool> {
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
let data = self.get_login_data().ok_or(anyhow!("No login data set!"))?;
let request = Request::post(format!("{}/login", server_url))
.timeout(Duration::from_secs(10))
.header("Content-Type", "application/json")
.body(serde_json::to_string(&data)?)?;
let mut response = isahc::send_async(request).await?;
let success = match response.status() {
StatusCode::OK => {
let token = response.text_async().await?;
self.set_token(&token);
true
}
StatusCode::UNAUTHORIZED => false,
_ => bail!("Unexpected response status!"),
};
Ok(success)
}
/// Make an unauthenticated get request to the server.
async fn get(&self, url: &str) -> Result<String> {
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
let mut response = Request::get(format!("{}/{}", server_url, url))
.timeout(Duration::from_secs(10))
.body(())?
.send_async()
.await?;
let body = response.text_async().await?;
Ok(body)
}
/// Make an authenticated post request to the server.
async fn post(&self, url: &str, body: String) -> Result<String> {
let body = match self.get_token() {
Some(_) => {
let mut response = self.post_priv(url, body.clone()).await?;
// Try one more time (maybe the token was expired)
if response.status() == StatusCode::UNAUTHORIZED {
if self.login().await? {
response = self.post_priv(url, body).await?;
} else {
bail!("Login failed!");
}
}
response.text_async().await?
}
None => {
let mut response = if self.login().await? {
self.post_priv(url, body).await?
} else {
bail!("Login failed!");
};
response.text_async().await?
}
};
Ok(body)
}
/// Post something to the server assuming there is a valid login token.
async fn post_priv(&self, url: &str, body: String) -> Result<Response<Body>> {
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
let token = self.get_token().ok_or(anyhow!("No login token!"))?;
let response = Request::post(format!("{}/{}", server_url, url))
.timeout(Duration::from_secs(10))
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.body(body)?
.send_async()
.await?;
Ok(response)
}
}

View file

@ -1,18 +0,0 @@
use super::Backend;
use crate::database::Person;
use anyhow::Result;
impl Backend {
/// Get all available persons from the server.
pub async fn get_persons(&self) -> Result<Vec<Person>> {
let body = self.get("persons").await?;
let persons: Vec<Person> = serde_json::from_str(&body)?;
Ok(persons)
}
/// Post a new person to the server.
pub async fn post_person(&self, data: &Person) -> Result<()> {
self.post("persons", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,18 +0,0 @@
use super::Backend;
use crate::database::Recording;
use anyhow::Result;
impl Backend {
/// Get all available recordings from the server.
pub async fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> {
let body = self.get(&format!("works/{}/recordings", work_id)).await?;
let recordings: Vec<Recording> = serde_json::from_str(&body)?;
Ok(recordings)
}
/// Post a new recording to the server.
pub async fn post_recording(&self, data: &Recording) -> Result<()> {
self.post("recordings", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,18 +0,0 @@
use super::Backend;
use crate::database::Work;
use anyhow::Result;
impl Backend {
/// Get all available works from the server.
pub async fn get_works(&self, composer_id: &str) -> Result<Vec<Work>> {
let body = self.get(&format!("persons/{}/works", composer_id)).await?;
let works: Vec<Work> = serde_json::from_str(&body)?;
Ok(works)
}
/// Post a new work to the server.
pub async fn post_work(&self, data: &Work) -> Result<()> {
self.post("works", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,83 +0,0 @@
use super::{Backend, BackendState};
use crate::database::DbThread;
use crate::player::Player;
use anyhow::Result;
use gio::prelude::*;
use std::path::PathBuf;
use std::rc::Rc;
impl Backend {
/// Initialize the music library if it is set in the settings.
pub(super) async fn init_library(&self) -> Result<()> {
if let Some(path) = self.settings.get_string("music-library-path") {
if !path.is_empty() {
self.set_music_library_path_priv(PathBuf::from(path.to_string()))
.await?;
}
}
Ok(())
}
/// Set the path to the music library folder and start a database thread in the background.
pub async fn set_music_library_path(&self, path: PathBuf) -> Result<()> {
self.settings
.set_string("music-library-path", path.to_str().unwrap())?;
self.set_music_library_path_priv(path).await
}
/// Set the path to the music library folder and start a database thread in the background.
pub async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> {
self.set_state(BackendState::Loading);
if let Some(db) = &*self.database.borrow() {
db.stop().await?;
}
self.music_library_path.replace(Some(path.clone()));
let mut db_path = path.clone();
db_path.push("musicus.db");
let database = DbThread::new(db_path.to_str().unwrap().to_string()).await?;
self.database.replace(Some(Rc::new(database)));
let player = Player::new(path);
self.player.replace(Some(player));
self.set_state(BackendState::Ready);
Ok(())
}
/// Get the currently set music library path.
pub fn get_music_library_path(&self) -> Option<PathBuf> {
self.music_library_path.borrow().clone()
}
/// Get an interface to the current music library database.
pub fn get_database(&self) -> Option<Rc<DbThread>> {
self.database.borrow().clone()
}
/// Get an interface to the database and panic if there is none.
pub fn db(&self) -> Rc<DbThread> {
self.get_database().unwrap()
}
/// Get an interface to the playback service.
pub fn get_player(&self) -> Option<Rc<Player>> {
self.player.borrow().clone()
}
/// Notify the frontend that the library was changed.
pub fn library_changed(&self) {
self.set_state(BackendState::Loading);
self.set_state(BackendState::Ready);
}
/// Get an interface to the player and panic if there is none.
pub fn pl(&self) -> Rc<Player> {
self.get_player().unwrap()
}
}

View file

@ -1,76 +0,0 @@
use crate::database::DbThread;
use crate::player::Player;
use anyhow::Result;
use futures_channel::mpsc;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
pub mod client;
pub use client::*;
pub mod library;
pub use library::*;
mod secure;
/// General states the application can be in.
pub enum BackendState {
/// The backend is not set up yet. This means that no backend methods except for setting the
/// music library path should be called. The user interface should adapt and only present this
/// option.
NoMusicLibrary,
/// The backend is loading the music library. No methods should be called. The user interface
/// should represent that state by prohibiting all interaction.
Loading,
/// The backend is ready and all methods may be called.
Ready,
}
/// A collection of all backend state and functionality.
pub struct Backend {
pub state_stream: RefCell<mpsc::Receiver<BackendState>>,
state_sender: RefCell<mpsc::Sender<BackendState>>,
settings: gio::Settings,
music_library_path: RefCell<Option<PathBuf>>,
database: RefCell<Option<Rc<DbThread>>>,
player: RefCell<Option<Rc<Player>>>,
server_url: RefCell<Option<String>>,
login_data: RefCell<Option<LoginData>>,
token: RefCell<Option<String>>,
}
impl Backend {
/// Create a new backend initerface. The user interface should subscribe to the state stream
/// and call init() afterwards.
pub fn new() -> Self {
let (state_sender, state_stream) = mpsc::channel(1024);
Backend {
state_stream: RefCell::new(state_stream),
state_sender: RefCell::new(state_sender),
settings: gio::Settings::new("de.johrpan.musicus"),
music_library_path: RefCell::new(None),
database: RefCell::new(None),
player: RefCell::new(None),
server_url: RefCell::new(None),
login_data: RefCell::new(None),
token: RefCell::new(None),
}
}
/// Initialize the backend updating the state accordingly.
pub async fn init(self: Rc<Backend>) -> Result<()> {
self.init_library().await?;
self.init_client()?;
Ok(())
}
/// Set the current state and notify the user interface.
fn set_state(&self, state: BackendState) {
self.state_sender.borrow_mut().try_send(state).unwrap();
}
}

View file

@ -1,108 +0,0 @@
use super::LoginData;
use anyhow::{anyhow, Result};
use futures_channel::oneshot;
use secret_service::{Collection, EncryptionType, SecretService};
/// Savely store the user's current login credentials.
pub async fn store_login_data(data: LoginData) -> Result<()> {
let (sender, receiver) = oneshot::channel::<Result<()>>();
std::thread::spawn(move || sender.send(store_login_data_priv(data)));
receiver.await?
}
/// Savely store the user's current login credentials.
fn store_login_data_priv(data: LoginData) -> Result<()> {
let ss = get_ss()?;
let collection = get_collection(&ss)?;
let key = "musicus-login-data";
delete_secrets(&collection, key)?;
collection
.create_item(
key,
vec![("username", &data.username)],
data.password.as_bytes(),
true,
"text/plain",
)
.or(Err(anyhow!(
"Failed to save login data using SecretService!"
)))?;
Ok(())
}
/// Get the login credentials from secret storage.
pub fn load_login_data() -> Result<Option<LoginData>> {
let ss = get_ss()?;
let collection = get_collection(&ss)?;
let items = collection.get_all_items().or(Err(anyhow!(
"Failed to get items from SecretService collection!"
)))?;
let key = "musicus-login-data";
let item = items
.iter()
.find(|item| item.get_label().unwrap_or_default() == key);
Ok(match item {
Some(item) => {
let attrs = item.get_attributes().or(Err(anyhow!(
"Failed to get attributes for ScretService item!"
)))?;
let username = attrs
.iter()
.find(|attr| attr.0 == "username")
.ok_or(anyhow!("No username in login data!"))?
.1
.clone();
let password = std::str::from_utf8(
&item
.get_secret()
.or(Err(anyhow!("Failed to get secret from SecretService!")))?,
)?
.to_string();
Some(LoginData { username, password })
}
None => None,
})
}
/// Delete all stored secrets for the provided key.
fn delete_secrets(collection: &Collection, key: &str) -> Result<()> {
let items = collection.get_all_items().or(Err(anyhow!(
"Failed to get items from SecretService collection!"
)))?;
for item in items {
if item.get_label().unwrap_or_default() == key {
item.delete()
.or(Err(anyhow!("Failed to delete SecretService item!")))?;
}
}
Ok(())
}
/// Get the SecretService interface.
fn get_ss() -> Result<SecretService> {
SecretService::new(EncryptionType::Dh).or(Err(anyhow!("Failed to get SecretService!")))
}
/// Get the default SecretService collection and unlock it.
fn get_collection(ss: &SecretService) -> Result<Collection> {
let collection = ss
.get_default_collection()
.or(Err(anyhow!("Failed to get SecretService connection!")))?;
collection
.unlock()
.or(Err(anyhow!("Failed to unclock SecretService collection!")))?;
Ok(collection)
}

View file

@ -1,2 +0,0 @@
pub static VERSION: &str = @VERSION@;
pub static LOCALEDIR: &str = @LOCALEDIR@;

View file

@ -1,53 +0,0 @@
use super::schema::ensembles;
use super::Database;
use anyhow::Result;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// An ensemble that takes part in recordings.
#[derive(Serialize, Deserialize, Insertable, Queryable, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Ensemble {
pub id: String,
pub name: String,
}
impl Database {
/// Update an existing ensemble or insert a new one.
pub fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
diesel::replace_into(ensembles::table)
.values(ensemble)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing ensemble.
pub fn get_ensemble(&self, id: &str) -> Result<Option<Ensemble>> {
let ensemble = ensembles::table
.filter(ensembles::id.eq(id))
.load::<Ensemble>(&self.connection)?
.into_iter()
.next();
Ok(ensemble)
}
/// Delete an existing ensemble.
pub fn delete_ensemble(&self, id: &str) -> Result<()> {
diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all existing ensembles.
pub fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
let ensembles = ensembles::table.load::<Ensemble>(&self.connection)?;
Ok(ensembles)
}
}

View file

@ -1,54 +0,0 @@
use super::schema::instruments;
use super::Database;
use anyhow::Result;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// An instrument or any other possible role within a recording.
#[derive(Serialize, Deserialize, Insertable, Queryable, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Instrument {
pub id: String,
pub name: String,
}
impl Database {
/// Update an existing instrument or insert a new one.
pub fn update_instrument(&self, instrument: Instrument) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
diesel::replace_into(instruments::table)
.values(instrument)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing instrument.
pub fn get_instrument(&self, id: &str) -> Result<Option<Instrument>> {
let instrument = instruments::table
.filter(instruments::id.eq(id))
.load::<Instrument>(&self.connection)?
.into_iter()
.next();
Ok(instrument)
}
/// Delete an existing instrument.
pub fn delete_instrument(&self, id: &str) -> Result<()> {
diesel::delete(instruments::table.filter(instruments::id.eq(id)))
.execute(&self.connection)?;
Ok(())
}
/// Get all existing instruments.
pub fn get_instruments(&self) -> Result<Vec<Instrument>> {
let instruments = instruments::table.load::<Instrument>(&self.connection)?;
Ok(instruments)
}
}

View file

@ -1,254 +0,0 @@
use super::generate_id;
use super::schema::{mediums, recordings, track_sets, tracks};
use super::{Database, Recording};
use anyhow::{anyhow, Error, Result};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// Representation of someting like a physical audio disc or a folder with
/// audio files (i.e. a collection of tracks for one or more recordings).
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Medium {
/// An unique ID for the medium.
pub id: String,
/// The human identifier for the medium.
pub name: String,
/// If applicable, the MusicBrainz DiscID.
pub discid: Option<String>,
/// The tracks of the medium, grouped by recording.
pub tracks: Vec<TrackSet>,
}
/// A set of tracks of one recording within a medium.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TrackSet {
/// The recording to which the tracks belong.
pub recording: Recording,
/// The actual tracks.
pub tracks: Vec<Track>,
}
/// A track within a recording on a medium.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Track {
/// The work parts that are played on this track. They are indices to the
/// work parts of the work that is associated with the recording.
pub work_parts: Vec<usize>,
/// The path to the audio file containing this track. This will not be
/// included when communicating with the server.
#[serde(skip)]
pub path: String,
}
/// Table data for a [`Medium`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "mediums"]
struct MediumRow {
pub id: String,
pub name: String,
pub discid: Option<String>,
}
/// Table data for a [`TrackSet`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "track_sets"]
struct TrackSetRow {
pub id: String,
pub medium: String,
pub index: i32,
pub recording: String,
}
/// Table data for a [`Track`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "tracks"]
struct TrackRow {
pub id: String,
pub track_set: String,
pub index: i32,
pub work_parts: String,
pub path: String,
}
impl Database {
/// Update an existing medium or insert a new one.
pub fn update_medium(&self, medium: Medium) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), Error, _>(|| {
let medium_id = &medium.id;
// This will also delete the track sets and tracks.
self.delete_medium(medium_id)?;
// Add the new medium.
let medium_row = MediumRow {
id: medium_id.to_owned(),
name: medium.name.clone(),
discid: medium.discid.clone(),
};
diesel::insert_into(mediums::table)
.values(medium_row)
.execute(&self.connection)?;
for (index, track_set) in medium.tracks.iter().enumerate() {
// Add associated items from the server, if they don't already
// exist.
if self.get_recording(&track_set.recording.id)?.is_none() {
self.update_recording(track_set.recording.clone())?;
}
// Add the actual track set data.
let track_set_id = generate_id();
let track_set_row = TrackSetRow {
id: track_set_id.clone(),
medium: medium_id.to_owned(),
index: index as i32,
recording: track_set.recording.id.clone(),
};
diesel::insert_into(track_sets::table)
.values(track_set_row)
.execute(&self.connection)?;
for (index, track) in track_set.tracks.iter().enumerate() {
let work_parts = track
.work_parts
.iter()
.map(|part_index| part_index.to_string())
.collect::<Vec<String>>()
.join(",");
let track_row = TrackRow {
id: generate_id(),
track_set: track_set_id.clone(),
index: index as i32,
work_parts,
path: track.path.clone(),
};
diesel::insert_into(tracks::table)
.values(track_row)
.execute(&self.connection)?;
}
}
Ok(())
})?;
Ok(())
}
/// Get an existing medium.
pub fn get_medium(&self, id: &str) -> Result<Option<Medium>> {
let row = mediums::table
.filter(mediums::id.eq(id))
.load::<MediumRow>(&self.connection)?
.into_iter()
.next();
let medium = match row {
Some(row) => Some(self.get_medium_data(row)?),
None => None,
};
Ok(medium)
}
/// Delete a medium and all of its tracks. This will fail, if the music
/// library contains audio files referencing any of those tracks.
pub fn delete_medium(&self, id: &str) -> Result<()> {
diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all available track sets for a recording.
pub fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
let mut track_sets: Vec<TrackSet> = Vec::new();
let rows = track_sets::table
.inner_join(recordings::table.on(recordings::id.eq(track_sets::recording)))
.filter(recordings::id.eq(recording_id))
.select(track_sets::table::all_columns())
.load::<TrackSetRow>(&self.connection)?;
for row in rows {
let track_set = self.get_track_set_from_row(row)?;
track_sets.push(track_set);
}
Ok(track_sets)
}
/// Retrieve all available information on a medium from related tables.
fn get_medium_data(&self, row: MediumRow) -> Result<Medium> {
let track_set_rows = track_sets::table
.filter(track_sets::medium.eq(&row.id))
.order_by(track_sets::index)
.load::<TrackSetRow>(&self.connection)?;
let mut track_sets = Vec::new();
for track_set_row in track_set_rows {
let track_set = self.get_track_set_from_row(track_set_row)?;
track_sets.push(track_set);
}
let medium = Medium {
id: row.id,
name: row.name,
discid: row.discid,
tracks: track_sets,
};
Ok(medium)
}
/// Convert a track set row from the database to an actual track set.
fn get_track_set_from_row(&self, row: TrackSetRow) -> Result<TrackSet> {
let recording_id = row.recording;
let recording = self
.get_recording(&recording_id)?
.ok_or_else(|| anyhow!("No recording with ID: {}", recording_id))?;
let track_rows = tracks::table
.filter(tracks::track_set.eq(row.id))
.order_by(tracks::index)
.load::<TrackRow>(&self.connection)?;
let mut tracks = Vec::new();
for track_row in track_rows {
let work_parts = track_row
.work_parts
.split(',')
.map(|part_index| Ok(str::parse(part_index)?))
.collect::<Result<Vec<usize>>>()?;
let track = Track {
work_parts,
path: track_row.path,
};
tracks.push(track);
}
let track_set = TrackSet { recording, tracks };
Ok(track_set)
}
}

View file

@ -1,59 +0,0 @@
use anyhow::Result;
use diesel::prelude::*;
pub mod ensembles;
pub use ensembles::*;
pub mod instruments;
pub use instruments::*;
pub mod medium;
pub use medium::*;
pub mod persons;
pub use persons::*;
pub mod recordings;
pub use recordings::*;
pub mod thread;
pub use thread::*;
pub mod works;
pub use works::*;
mod schema;
// This makes the SQL migration scripts accessible from the code.
embed_migrations!();
/// Generate a random string suitable as an item ID.
pub fn generate_id() -> String {
let mut buffer = uuid::Uuid::encode_buffer();
let id = uuid::Uuid::new_v4().to_simple().encode_lower(&mut buffer);
id.to_string()
}
/// Interface to a Musicus database.
pub struct Database {
connection: SqliteConnection,
}
impl Database {
/// Create a new database interface and run migrations if necessary.
pub fn new(file_name: &str) -> Result<Database> {
let connection = SqliteConnection::establish(file_name)?;
diesel::sql_query("PRAGMA foreign_keys = ON").execute(&connection)?;
embedded_migrations::run(&connection)?;
Ok(Database { connection })
}
/// Defer all foreign keys for the next transaction.
fn defer_foreign_keys(&self) -> Result<()> {
diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(&self.connection)?;
Ok(())
}
}

View file

@ -1,66 +0,0 @@
use super::schema::persons;
use super::Database;
use anyhow::Result;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// A person that is a composer, an interpret or both.
#[derive(Serialize, Deserialize, Insertable, Queryable, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Person {
pub id: String,
pub first_name: String,
pub last_name: String,
}
impl Person {
/// Get the full name in the form "First Last".
pub fn name_fl(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
/// Get the full name in the form "Last, First".
pub fn name_lf(&self) -> String {
format!("{}, {}", self.last_name, self.first_name)
}
}
impl Database {
/// Update an existing person or insert a new one.
pub fn update_person(&self, person: Person) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
diesel::replace_into(persons::table)
.values(person)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing person.
pub fn get_person(&self, id: &str) -> Result<Option<Person>> {
let person = persons::table
.filter(persons::id.eq(id))
.load::<Person>(&self.connection)?
.into_iter()
.next();
Ok(person)
}
/// Delete an existing person.
pub fn delete_person(&self, id: &str) -> Result<()> {
diesel::delete(persons::table.filter(persons::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all existing persons.
pub fn get_persons(&self) -> Result<Vec<Person>> {
let persons = persons::table.load::<Person>(&self.connection)?;
Ok(persons)
}
}

View file

@ -1,316 +0,0 @@
use super::generate_id;
use super::schema::{ensembles, performances, persons, recordings};
use super::{Database, Ensemble, Instrument, Person, Work};
use anyhow::{anyhow, Error, Result};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// Database table data for a recording.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "recordings"]
struct RecordingRow {
pub id: String,
pub work: String,
pub comment: String,
}
impl From<Recording> for RecordingRow {
fn from(recording: Recording) -> Self {
RecordingRow {
id: recording.id,
work: recording.work.id,
comment: recording.comment,
}
}
}
/// Database table data for a performance.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "performances"]
struct PerformanceRow {
pub id: i64,
pub recording: String,
pub person: Option<String>,
pub ensemble: Option<String>,
pub role: Option<String>,
}
/// How a person or ensemble was involved in a recording.
// TODO: Replace person/ensemble with an enum.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Performance {
pub person: Option<Person>,
pub ensemble: Option<Ensemble>,
pub role: Option<Instrument>,
}
impl Performance {
/// Get a string representation of the performance.
// TODO: Replace with impl Display.
pub fn get_title(&self) -> String {
let mut text = String::from(if self.is_person() {
self.unwrap_person().name_fl()
} else {
self.unwrap_ensemble().name
});
if self.has_role() {
text = text + " (" + &self.unwrap_role().name + ")";
}
text
}
pub fn is_person(&self) -> bool {
self.person.is_some()
}
pub fn unwrap_person(&self) -> Person {
self.person.clone().unwrap()
}
pub fn unwrap_ensemble(&self) -> Ensemble {
self.ensemble.clone().unwrap()
}
pub fn has_role(&self) -> bool {
self.role.clone().is_some()
}
pub fn unwrap_role(&self) -> Instrument {
self.role.clone().unwrap()
}
}
/// A specific recording of a work.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Recording {
pub id: String,
pub work: Work,
pub comment: String,
pub performances: Vec<Performance>,
}
impl Recording {
/// Initialize a new recording with a work.
pub fn new(work: Work) -> Self {
Self {
id: generate_id(),
work,
comment: String::new(),
performances: Vec::new(),
}
}
/// Get a string representation of the performances in this recording.
// TODO: Maybe replace with impl Display?
pub fn get_performers(&self) -> String {
let texts: Vec<String> = self
.performances
.iter()
.map(|performance| performance.get_title())
.collect();
texts.join(", ")
}
}
impl Database {
/// Update an existing recording or insert a new one.
// TODO: Think about whether to also insert the other items.
pub fn update_recording(&self, recording: Recording) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), Error, _>(|| {
let recording_id = &recording.id;
self.delete_recording(recording_id)?;
// Add associated items from the server, if they don't already exist.
if self.get_work(&recording.work.id)?.is_none() {
self.update_work(recording.work.clone())?;
}
for performance in &recording.performances {
if let Some(person) = &performance.person {
if self.get_person(&person.id)?.is_none() {
self.update_person(person.clone())?;
}
}
if let Some(ensemble) = &performance.ensemble {
if self.get_ensemble(&ensemble.id)?.is_none() {
self.update_ensemble(ensemble.clone())?;
}
}
if let Some(role) = &performance.role {
if self.get_instrument(&role.id)?.is_none() {
self.update_instrument(role.clone())?;
}
}
}
// Add the actual recording.
let row: RecordingRow = recording.clone().into();
diesel::insert_into(recordings::table)
.values(row)
.execute(&self.connection)?;
for performance in recording.performances {
let row = PerformanceRow {
id: rand::random(),
recording: recording_id.to_string(),
person: performance.person.map(|person| person.id),
ensemble: performance.ensemble.map(|ensemble| ensemble.id),
role: performance.role.map(|role| role.id),
};
diesel::insert_into(performances::table)
.values(row)
.execute(&self.connection)?;
}
Ok(())
})?;
Ok(())
}
/// Check whether the database contains a recording.
pub fn recording_exists(&self, id: &str) -> Result<bool> {
let exists = recordings::table
.filter(recordings::id.eq(id))
.load::<RecordingRow>(&self.connection)?
.first()
.is_some();
Ok(exists)
}
/// Get an existing recording.
pub fn get_recording(&self, id: &str) -> Result<Option<Recording>> {
let row = recordings::table
.filter(recordings::id.eq(id))
.load::<RecordingRow>(&self.connection)?
.into_iter()
.next();
let recording = match row {
Some(row) => Some(self.get_recording_data(row)?),
None => None,
};
Ok(recording)
}
/// Retrieve all available information on a recording from related tables.
fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> {
let mut performance_descriptions: Vec<Performance> = Vec::new();
let performance_rows = performances::table
.filter(performances::recording.eq(&row.id))
.load::<PerformanceRow>(&self.connection)?;
for row in performance_rows {
performance_descriptions.push(Performance {
person: match row.person {
Some(id) => Some(
self.get_person(&id)?
.ok_or(anyhow!("No person with ID: {}", id))?,
),
None => None,
},
ensemble: match row.ensemble {
Some(id) => Some(
self.get_ensemble(&id)?
.ok_or(anyhow!("No ensemble with ID: {}", id))?,
),
None => None,
},
role: match row.role {
Some(id) => Some(
self.get_instrument(&id)?
.ok_or(anyhow!("No instrument with ID: {}", id))?,
),
None => None,
},
});
}
let work_id = &row.work;
let work = self
.get_work(work_id)?
.ok_or(anyhow!("Work doesn't exist: {}", work_id))?;
let recording_description = Recording {
id: row.id,
work,
comment: row.comment.clone(),
performances: performance_descriptions,
};
Ok(recording_description)
}
/// Get all available information on all recordings where a person is performing.
pub fn get_recordings_for_person(&self, person_id: &str) -> Result<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
.inner_join(persons::table.on(persons::id.nullable().eq(performances::person)))
.filter(persons::id.eq(person_id))
.select(recordings::table::all_columns())
.load::<RecordingRow>(&self.connection)?;
for row in rows {
recordings.push(self.get_recording_data(row)?);
}
Ok(recordings)
}
/// Get all available information on all recordings where an ensemble is performing.
pub fn get_recordings_for_ensemble(&self, ensemble_id: &str) -> Result<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
.inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble)))
.filter(ensembles::id.eq(ensemble_id))
.select(recordings::table::all_columns())
.load::<RecordingRow>(&self.connection)?;
for row in rows {
recordings.push(self.get_recording_data(row)?);
}
Ok(recordings)
}
/// Get allavailable information on all recordings of a work.
pub fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.filter(recordings::work.eq(work_id))
.load::<RecordingRow>(&self.connection)?;
for row in rows {
recordings.push(self.get_recording_data(row)?);
}
Ok(recordings)
}
/// Delete an existing recording. This will fail if there are still references to this
/// recording from other tables that are not directly part of the recording data.
pub fn delete_recording(&self, id: &str) -> Result<()> {
diesel::delete(recordings::table.filter(recordings::id.eq(id)))
.execute(&self.connection)?;
Ok(())
}
}

View file

@ -1,131 +0,0 @@
table! {
ensembles (id) {
id -> Text,
name -> Text,
}
}
table! {
instrumentations (id) {
id -> BigInt,
work -> Text,
instrument -> Text,
}
}
table! {
instruments (id) {
id -> Text,
name -> Text,
}
}
table! {
mediums (id) {
id -> Text,
name -> Text,
discid -> Nullable<Text>,
}
}
table! {
performances (id) {
id -> BigInt,
recording -> Text,
person -> Nullable<Text>,
ensemble -> Nullable<Text>,
role -> Nullable<Text>,
}
}
table! {
persons (id) {
id -> Text,
first_name -> Text,
last_name -> Text,
}
}
table! {
recordings (id) {
id -> Text,
work -> Text,
comment -> Text,
}
}
table! {
track_sets (id) {
id -> Text,
medium -> Text,
index -> Integer,
recording -> Text,
}
}
table! {
tracks (id) {
id -> Text,
track_set -> Text,
index -> Integer,
work_parts -> Text,
path -> Text,
}
}
table! {
work_parts (id) {
id -> BigInt,
work -> Text,
part_index -> BigInt,
title -> Text,
composer -> Nullable<Text>,
}
}
table! {
work_sections (id) {
id -> BigInt,
work -> Text,
title -> Text,
before_index -> BigInt,
}
}
table! {
works (id) {
id -> Text,
composer -> Text,
title -> Text,
}
}
joinable!(instrumentations -> instruments (instrument));
joinable!(instrumentations -> works (work));
joinable!(performances -> ensembles (ensemble));
joinable!(performances -> instruments (role));
joinable!(performances -> persons (person));
joinable!(performances -> recordings (recording));
joinable!(recordings -> works (work));
joinable!(track_sets -> mediums (medium));
joinable!(track_sets -> recordings (recording));
joinable!(tracks -> track_sets (track_set));
joinable!(work_parts -> persons (composer));
joinable!(work_parts -> works (work));
joinable!(work_sections -> works (work));
joinable!(works -> persons (composer));
allow_tables_to_appear_in_same_query!(
ensembles,
instrumentations,
instruments,
mediums,
performances,
persons,
recordings,
track_sets,
tracks,
work_parts,
work_sections,
works,
);

View file

@ -1,355 +0,0 @@
use super::*;
use anyhow::Result;
use futures_channel::oneshot;
use futures_channel::oneshot::Sender;
use std::sync::mpsc;
use std::thread;
/// An action the database thread can perform.
enum Action {
UpdatePerson(Person, Sender<Result<()>>),
GetPerson(String, Sender<Result<Option<Person>>>),
DeletePerson(String, Sender<Result<()>>),
GetPersons(Sender<Result<Vec<Person>>>),
UpdateInstrument(Instrument, Sender<Result<()>>),
GetInstrument(String, Sender<Result<Option<Instrument>>>),
DeleteInstrument(String, Sender<Result<()>>),
GetInstruments(Sender<Result<Vec<Instrument>>>),
UpdateWork(Work, Sender<Result<()>>),
DeleteWork(String, Sender<Result<()>>),
GetWorks(String, Sender<Result<Vec<Work>>>),
UpdateEnsemble(Ensemble, Sender<Result<()>>),
GetEnsemble(String, Sender<Result<Option<Ensemble>>>),
DeleteEnsemble(String, Sender<Result<()>>),
GetEnsembles(Sender<Result<Vec<Ensemble>>>),
UpdateRecording(Recording, Sender<Result<()>>),
DeleteRecording(String, Sender<Result<()>>),
GetRecordingsForPerson(String, Sender<Result<Vec<Recording>>>),
GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>),
GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>),
RecordingExists(String, Sender<Result<bool>>),
UpdateMedium(Medium, Sender<Result<()>>),
GetMedium(String, Sender<Result<Option<Medium>>>),
DeleteMedium(String, Sender<Result<()>>),
GetTrackSets(String, Sender<Result<Vec<TrackSet>>>),
Stop(Sender<()>),
}
use Action::*;
/// A database running within a thread.
pub struct DbThread {
action_sender: mpsc::Sender<Action>,
}
impl DbThread {
/// Create a new database connection in a background thread.
pub async fn new(path: String) -> Result<Self> {
let (action_sender, action_receiver) = mpsc::channel();
let (ready_sender, ready_receiver) = oneshot::channel();
thread::spawn(move || {
let db = match Database::new(&path) {
Ok(db) => {
ready_sender.send(Ok(())).unwrap();
db
}
Err(error) => {
ready_sender.send(Err(error)).unwrap();
return;
}
};
for action in action_receiver {
match action {
UpdatePerson(person, sender) => {
sender.send(db.update_person(person)).unwrap();
}
GetPerson(id, sender) => {
sender.send(db.get_person(&id)).unwrap();
}
DeletePerson(id, sender) => {
sender.send(db.delete_person(&id)).unwrap();
}
GetPersons(sender) => {
sender.send(db.get_persons()).unwrap();
}
UpdateInstrument(instrument, sender) => {
sender.send(db.update_instrument(instrument)).unwrap();
}
GetInstrument(id, sender) => {
sender.send(db.get_instrument(&id)).unwrap();
}
DeleteInstrument(id, sender) => {
sender.send(db.delete_instrument(&id)).unwrap();
}
GetInstruments(sender) => {
sender.send(db.get_instruments()).unwrap();
}
UpdateWork(work, sender) => {
sender.send(db.update_work(work)).unwrap();
}
DeleteWork(id, sender) => {
sender.send(db.delete_work(&id)).unwrap();
}
GetWorks(id, sender) => {
sender.send(db.get_works(&id)).unwrap();
}
UpdateEnsemble(ensemble, sender) => {
sender.send(db.update_ensemble(ensemble)).unwrap();
}
GetEnsemble(id, sender) => {
sender.send(db.get_ensemble(&id)).unwrap();
}
DeleteEnsemble(id, sender) => {
sender.send(db.delete_ensemble(&id)).unwrap();
}
GetEnsembles(sender) => {
sender.send(db.get_ensembles()).unwrap();
}
UpdateRecording(recording, sender) => {
sender.send(db.update_recording(recording)).unwrap();
}
DeleteRecording(id, sender) => {
sender.send(db.delete_recording(&id)).unwrap();
}
GetRecordingsForPerson(id, sender) => {
sender.send(db.get_recordings_for_person(&id)).unwrap();
}
GetRecordingsForEnsemble(id, sender) => {
sender.send(db.get_recordings_for_ensemble(&id)).unwrap();
}
GetRecordingsForWork(id, sender) => {
sender.send(db.get_recordings_for_work(&id)).unwrap();
}
RecordingExists(id, sender) => {
sender.send(db.recording_exists(&id)).unwrap();
}
UpdateMedium(medium, sender) => {
sender.send(db.update_medium(medium)).unwrap();
}
GetMedium(id, sender) => {
sender.send(db.get_medium(&id)).unwrap();
}
DeleteMedium(id, sender) => {
sender.send(db.delete_medium(&id)).unwrap();
}
GetTrackSets(recording_id, sender) => {
sender.send(db.get_track_sets(&recording_id)).unwrap();
}
Stop(sender) => {
sender.send(()).unwrap();
break;
}
}
}
});
ready_receiver.await??;
Ok(Self { action_sender })
}
/// Update an existing person or insert a new one.
pub async fn update_person(&self, person: Person) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdatePerson(person, sender))?;
receiver.await?
}
/// Get an existing person.
pub async fn get_person(&self, id: &str) -> Result<Option<Person>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetPerson(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing person. This will fail, if there are still other items referencing
/// this person.
pub async fn delete_person(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeletePerson(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing persons.
pub async fn get_persons(&self) -> Result<Vec<Person>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetPersons(sender))?;
receiver.await?
}
/// Update an existing instrument or insert a new one.
pub async fn update_instrument(&self, instrument: Instrument) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(UpdateInstrument(instrument, sender))?;
receiver.await?
}
/// Get an existing instrument.
pub async fn get_instrument(&self, id: &str) -> Result<Option<Instrument>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetInstrument(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing instrument. This will fail, if there are still other items referencing
/// this instrument.
pub async fn delete_instrument(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteInstrument(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing instruments.
pub async fn get_instruments(&self) -> Result<Vec<Instrument>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetInstruments(sender))?;
receiver.await?
}
/// Update an existing work or insert a new one.
pub async fn update_work(&self, work: Work) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateWork(work, sender))?;
receiver.await?
}
/// Delete an existing work. This will fail, if there are still other items referencing
/// this work.
pub async fn delete_work(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteWork(id.to_string(), sender))?;
receiver.await?
}
/// Get information on all existing works by a composer.
pub async fn get_works(&self, person_id: &str) -> Result<Vec<Work>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetWorks(person_id.to_string(), sender))?;
receiver.await?
}
/// Update an existing ensemble or insert a new one.
pub async fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateEnsemble(ensemble, sender))?;
receiver.await?
}
/// Get an existing ensemble.
pub async fn get_ensemble(&self, id: &str) -> Result<Option<Ensemble>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetEnsemble(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing ensemble. This will fail, if there are still other items referencing
/// this ensemble.
pub async fn delete_ensemble(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteEnsemble(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing ensembles.
pub async fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetEnsembles(sender))?;
receiver.await?
}
/// Update an existing recording or insert a new one.
pub async fn update_recording(&self, recording: Recording) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(UpdateRecording(recording, sender))?;
receiver.await?
}
/// Delete an existing recording.
pub async fn delete_recording(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteRecording(id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings in which a person performs.
pub async fn get_recordings_for_person(&self, person_id: &str) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForPerson(person_id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings in which an ensemble performs.
pub async fn get_recordings_for_ensemble(&self, ensemble_id: &str) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForEnsemble(ensemble_id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings of a work.
pub async fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForWork(work_id.to_string(), sender))?;
receiver.await?
}
/// Check whether a recording exists within the database.
pub async fn recording_exists(&self, id: &str) -> Result<bool> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(RecordingExists(id.to_string(), sender))?;
receiver.await?
}
/// Update an existing medium or insert a new one.
pub async fn update_medium(&self, medium: Medium) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateMedium(medium, sender))?;
receiver.await?
}
/// Delete an existing medium. This will fail, if there are still other
/// items referencing this medium.
pub async fn delete_medium(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteMedium(id.to_owned(), sender))?;
receiver.await?
}
/// Get an existing medium.
pub async fn get_medium(&self, id: &str) -> Result<Option<Medium>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetMedium(id.to_owned(), sender))?;
receiver.await?
}
/// Get all track sets for a recording.
pub async fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetTrackSets(recording_id.to_owned(), sender))?;
receiver.await?
}
/// Stop the database thread. Any future access to the database will fail.
pub async fn stop(&self) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(Stop(sender))?;
Ok(receiver.await?)
}
}

View file

@ -1,297 +0,0 @@
use super::generate_id;
use super::schema::{instrumentations, work_parts, work_sections, works};
use super::{Database, Instrument, Person};
use anyhow::{anyhow, Error, Result};
use diesel::prelude::*;
use diesel::{Insertable, Queryable};
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
/// Table row data for a work.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "works"]
struct WorkRow {
pub id: String,
pub composer: String,
pub title: String,
}
impl From<Work> for WorkRow {
fn from(work: Work) -> Self {
WorkRow {
id: work.id,
composer: work.composer.id,
title: work.title,
}
}
}
/// Definition that a work uses an instrument.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "instrumentations"]
struct InstrumentationRow {
pub id: i64,
pub work: String,
pub instrument: String,
}
/// Table row data for a work part.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "work_parts"]
struct WorkPartRow {
pub id: i64,
pub work: String,
pub part_index: i64,
pub title: String,
pub composer: Option<String>,
}
/// Table row data for a work section.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "work_sections"]
struct WorkSectionRow {
pub id: i64,
pub work: String,
pub title: String,
pub before_index: i64,
}
/// A concrete work part that can be recorded.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct WorkPart {
pub title: String,
pub composer: Option<Person>,
}
/// A heading between work parts.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct WorkSection {
pub title: String,
pub before_index: usize,
}
/// A specific work by a composer.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Work {
pub id: String,
pub title: String,
pub composer: Person,
pub instruments: Vec<Instrument>,
pub parts: Vec<WorkPart>,
pub sections: Vec<WorkSection>,
}
impl Work {
/// Initialize a new work with a composer.
pub fn new(composer: Person) -> Self {
Self {
id: generate_id(),
title: String::new(),
composer,
instruments: Vec::new(),
parts: Vec::new(),
sections: Vec::new(),
}
}
/// Get a string including the composer and title of the work.
// TODO: Replace with impl Display.
pub fn get_title(&self) -> String {
format!("{}: {}", self.composer.name_fl(), self.title)
}
}
impl Database {
/// Update an existing work or insert a new one.
// TODO: Think about also inserting related items.
pub fn update_work(&self, work: Work) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), Error, _>(|| {
let work_id = &work.id;
self.delete_work(work_id)?;
// Add associated items from the server, if they don't already exist.
if self.get_person(&work.composer.id)?.is_none() {
self.update_person(work.composer.clone())?;
}
for instrument in &work.instruments {
if self.get_instrument(&instrument.id)?.is_none() {
self.update_instrument(instrument.clone())?;
}
}
for part in &work.parts {
if let Some(person) = &part.composer {
if self.get_person(&person.id)?.is_none() {
self.update_person(person.clone())?;
}
}
}
// Add the actual work.
let row: WorkRow = work.clone().into();
diesel::insert_into(works::table)
.values(row)
.execute(&self.connection)?;
match work {
Work {
instruments,
parts,
sections,
..
} => {
for instrument in instruments {
let row = InstrumentationRow {
id: rand::random(),
work: work_id.to_string(),
instrument: instrument.id,
};
diesel::insert_into(instrumentations::table)
.values(row)
.execute(&self.connection)?;
}
for (index, part) in parts.into_iter().enumerate() {
let row = WorkPartRow {
id: rand::random(),
work: work_id.to_string(),
part_index: index.try_into()?,
title: part.title,
composer: part.composer.map(|person| person.id),
};
diesel::insert_into(work_parts::table)
.values(row)
.execute(&self.connection)?;
}
for section in sections {
let row = WorkSectionRow {
id: rand::random(),
work: work_id.to_string(),
title: section.title,
before_index: section.before_index.try_into()?,
};
diesel::insert_into(work_sections::table)
.values(row)
.execute(&self.connection)?;
}
}
}
Ok(())
})?;
Ok(())
}
/// Get an existing work.
pub fn get_work(&self, id: &str) -> Result<Option<Work>> {
let row = works::table
.filter(works::id.eq(id))
.load::<WorkRow>(&self.connection)?
.first()
.cloned();
let work = match row {
Some(row) => Some(self.get_work_data(row)?),
None => None,
};
Ok(work)
}
/// Retrieve all available information on a work from related tables.
fn get_work_data(&self, row: WorkRow) -> Result<Work> {
let mut instruments: Vec<Instrument> = Vec::new();
let instrumentations = instrumentations::table
.filter(instrumentations::work.eq(&row.id))
.load::<InstrumentationRow>(&self.connection)?;
for instrumentation in instrumentations {
let id = &instrumentation.instrument;
instruments.push(
self.get_instrument(id)?
.ok_or(anyhow!("No instrument with ID: {}", id))?,
);
}
let mut parts: Vec<WorkPart> = Vec::new();
let part_rows = work_parts::table
.filter(work_parts::work.eq(&row.id))
.load::<WorkPartRow>(&self.connection)?;
for part_row in part_rows {
parts.push(WorkPart {
title: part_row.title,
composer: match part_row.composer {
Some(composer) => Some(
self.get_person(&composer)?
.ok_or(anyhow!("No person with ID: {}", composer))?,
),
None => None,
},
});
}
let mut sections: Vec<WorkSection> = Vec::new();
let section_rows = work_sections::table
.filter(work_sections::work.eq(&row.id))
.load::<WorkSectionRow>(&self.connection)?;
for section_row in section_rows {
sections.push(WorkSection {
title: section_row.title,
before_index: section_row.before_index.try_into()?,
});
}
let person_id = &row.composer;
let person = self
.get_person(person_id)?
.ok_or(anyhow!("Person doesn't exist: {}", person_id))?;
Ok(Work {
id: row.id,
composer: person,
title: row.title,
instruments,
parts,
sections,
})
}
/// Delete an existing work. This will fail if there are still other tables that relate to
/// this work except for the things that are part of the information on the work it
pub fn delete_work(&self, id: &str) -> Result<()> {
diesel::delete(works::table.filter(works::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all existing works by a composer and related information from other tables.
pub fn get_works(&self, composer_id: &str) -> Result<Vec<Work>> {
let mut works: Vec<Work> = Vec::new();
let rows = works::table
.filter(works::composer.eq(composer_id))
.load::<WorkRow>(&self.connection)?;
for row in rows {
works.push(self.get_work_data(row)?);
}
Ok(works)
}
}

View file

@ -1,22 +0,0 @@
use crate::config;
use gettextrs::gettext;
use gtk::prelude::*;
pub fn show_about_dialog<W: IsA<gtk::Window>>(parent: &W) {
let dialog = gtk::AboutDialogBuilder::new()
.transient_for(parent)
.modal(true)
.logo_icon_name("de.johrpan.musicus")
.program_name(&gettext("Musicus"))
.version(config::VERSION)
.comments(&gettext("The classical music player and organizer."))
.website("https://github.com/johrpan/musicus")
.website_label(&gettext("Further information and source code"))
.copyright("© 2020 Elias Projahn")
.license_type(gtk::License::Agpl30)
.authors(vec![String::from("Elias Projahn <johrpan@gmail.com>")])
.build();
dialog.connect_response(|dialog, _| dialog.close());
dialog.show();
}

View file

@ -1,88 +0,0 @@
use crate::backend::{Backend, LoginData};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for entering login credentials.
pub struct LoginDialog {
backend: Rc<Backend>,
window: libhandy::Window,
stack: gtk::Stack,
info_bar: gtk::InfoBar,
username_entry: gtk::Entry,
password_entry: gtk::Entry,
selected_cb: RefCell<Option<Box<dyn Fn(LoginData) -> ()>>>,
}
impl LoginDialog {
/// Create a new login dialog.
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/login_dialog.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, login_button);
get_widget!(builder, gtk::Entry, username_entry);
get_widget!(builder, gtk::Entry, password_entry);
window.set_transient_for(Some(parent));
let this = Rc::new(Self {
backend,
window,
stack,
info_bar,
username_entry,
password_entry,
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
login_button.connect_clicked(clone!(@strong this => move |_| {
this.stack.set_visible_child_name("loading");
let data = LoginData {
username: this.username_entry.get_text().to_string(),
password: this.password_entry.get_text().to_string(),
};
let c = glib::MainContext::default();
let clone = this.clone();
c.spawn_local(async move {
clone.backend.set_login_data(data.clone()).await.unwrap();
if clone.backend.login().await.unwrap() {
if let Some(cb) = &*clone.selected_cb.borrow() {
cb(data);
}
clone.window.close();
} else {
clone.stack.set_visible_child_name("content");
clone.info_bar.set_revealed(true);
}
});
}));
this
}
/// The closure to call when the login succeded.
pub fn set_selected_cb<F: Fn(LoginData) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the login dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,11 +0,0 @@
pub mod about;
pub use about::*;
pub mod login_dialog;
pub use login_dialog::*;
pub mod preferences;
pub use preferences::*;
pub mod server_dialog;
pub use server_dialog::*;

View file

@ -1,105 +0,0 @@
use super::{LoginDialog, ServerDialog};
use crate::backend::Backend;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::rc::Rc;
/// A dialog for configuring the app.
pub struct Preferences {
backend: Rc<Backend>,
window: libhandy::Window,
music_library_path_row: libhandy::ActionRow,
url_row: libhandy::ActionRow,
login_row: libhandy::ActionRow,
}
impl Preferences {
/// Create a new preferences dialog.
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/preferences.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, libhandy::ActionRow, music_library_path_row);
get_widget!(builder, gtk::Button, select_music_library_path_button);
get_widget!(builder, libhandy::ActionRow, url_row);
get_widget!(builder, gtk::Button, url_button);
get_widget!(builder, libhandy::ActionRow, login_row);
get_widget!(builder, gtk::Button, login_button);
window.set_transient_for(Some(parent));
let this = Rc::new(Self {
backend,
window,
music_library_path_row,
url_row,
login_row,
});
// Connect signals and callbacks
select_music_library_path_button.connect_clicked(clone!(@strong this => move |_| {
let dialog = gtk::FileChooserNative::new(
Some(&gettext("Select music library folder")),
Some(&this.window), gtk::FileChooserAction::SelectFolder,None, None);
if let gtk::ResponseType::Accept = dialog.run() {
if let Some(path) = dialog.get_filename() {
this.music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
let context = glib::MainContext::default();
let backend = this.backend.clone();
context.spawn_local(async move {
backend.set_music_library_path(path).await.unwrap();
});
}
}
}));
url_button.connect_clicked(clone!(@strong this => move |_| {
let dialog = ServerDialog::new(this.backend.clone(), &this.window);
dialog.set_selected_cb(clone!(@strong this => move |url| {
this.url_row.set_subtitle(Some(&url));
}));
dialog.show();
}));
login_button.connect_clicked(clone!(@strong this => move |_| {
let dialog = LoginDialog::new(this.backend.clone(), &this.window);
dialog.set_selected_cb(clone!(@strong this => move |data| {
this.login_row.set_subtitle(Some(&data.username));
}));
dialog.show();
}));
// Initialize
if let Some(path) = this.backend.get_music_library_path() {
this.music_library_path_row
.set_subtitle(Some(path.to_str().unwrap()));
}
if let Some(url) = this.backend.get_server_url() {
this.url_row.set_subtitle(Some(&url));
}
if let Some(data) = this.backend.get_login_data() {
this.login_row.set_subtitle(Some(&data.username));
}
this
}
/// Show the preferences dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,65 +0,0 @@
use crate::backend::Backend;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for setting up the server.
pub struct ServerDialog {
backend: Rc<Backend>,
window: libhandy::Window,
url_entry: gtk::Entry,
selected_cb: RefCell<Option<Box<dyn Fn(String) -> ()>>>,
}
impl ServerDialog {
/// Create a new server dialog.
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/server_dialog.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, set_button);
get_widget!(builder, gtk::Entry, url_entry);
window.set_transient_for(Some(parent));
let this = Rc::new(Self {
backend,
window,
url_entry,
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
set_button.connect_clicked(clone!(@strong this => move |_| {
let url = this.url_entry.get_text().to_string();
this.backend.set_server_url(&url).unwrap();
if let Some(cb) = &*this.selected_cb.borrow() {
cb(url);
}
this.window.close();
}));
this
}
/// The closure to call when the server was set.
pub fn set_selected_cb<F: Fn(String) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the server dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,135 +0,0 @@
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::{Navigator, NavigatorScreen};
use anyhow::Result;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a ensemble.
pub struct EnsembleEditor {
backend: Rc<Backend>,
id: String,
widget: gtk::Stack,
info_bar: gtk::InfoBar,
name_entry: gtk::Entry,
upload_switch: gtk::Switch,
saved_cb: RefCell<Option<Box<dyn Fn(Ensemble) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl EnsembleEditor {
/// Create a new ensemble editor and optionally initialize it.
pub fn new(backend: Rc<Backend>, ensemble: Option<Ensemble>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Entry, name_entry);
get_widget!(builder, gtk::Switch, upload_switch);
let id = match ensemble {
Some(ensemble) => {
name_entry.set_text(&ensemble.name);
ensemble.id
}
None => generate_id(),
};
let this = Rc::new(Self {
backend,
id,
widget,
info_bar,
name_entry,
upload_switch,
saved_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
save_button.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.widget.set_visible_child_name("loading");
match clone.clone().save().await {
Ok(_) => {
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.widget.set_visible_child_name("content");
}
}
});
}));
this
}
/// Set the closure to be called if the ensemble was saved.
pub fn set_saved_cb<F: Fn(Ensemble) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}
/// Save the ensemble and possibly upload it to the server.
async fn save(self: Rc<Self>) -> Result<()> {
let name = self.name_entry.get_text().to_string();
let ensemble = Ensemble {
id: self.id.clone(),
name,
};
let upload = self.upload_switch.get_active();
if upload {
self.backend.post_ensemble(&ensemble).await?;
}
self.backend
.db()
.update_ensemble(ensemble.clone())
.await?;
self.backend.library_changed();
if let Some(cb) = &*self.saved_cb.borrow() {
cb(ensemble.clone());
}
Ok(())
}
}
impl NavigatorScreen for EnsembleEditor {
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,135 +0,0 @@
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::{Navigator, NavigatorScreen};
use anyhow::Result;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a instrument.
pub struct InstrumentEditor {
backend: Rc<Backend>,
id: String,
widget: gtk::Stack,
info_bar: gtk::InfoBar,
name_entry: gtk::Entry,
upload_switch: gtk::Switch,
saved_cb: RefCell<Option<Box<dyn Fn(Instrument) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl InstrumentEditor {
/// Create a new instrument editor and optionally initialize it.
pub fn new(backend: Rc<Backend>, instrument: Option<Instrument>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Entry, name_entry);
get_widget!(builder, gtk::Switch, upload_switch);
let id = match instrument {
Some(instrument) => {
name_entry.set_text(&instrument.name);
instrument.id
}
None => generate_id(),
};
let this = Rc::new(Self {
backend,
id,
widget,
info_bar,
name_entry,
upload_switch,
saved_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
save_button.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.widget.set_visible_child_name("loading");
match clone.clone().save().await {
Ok(_) => {
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.widget.set_visible_child_name("content");
}
}
});
}));
this
}
/// Set the closure to be called if the instrument was saved.
pub fn set_saved_cb<F: Fn(Instrument) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}
/// Save the instrument and possibly upload it to the server.
async fn save(self: Rc<Self>) -> Result<()> {
let name = self.name_entry.get_text().to_string();
let instrument = Instrument {
id: self.id.clone(),
name,
};
let upload = self.upload_switch.get_active();
if upload {
self.backend.post_instrument(&instrument).await?;
}
self.backend
.db()
.update_instrument(instrument.clone())
.await?;
self.backend.library_changed();
if let Some(cb) = &*self.saved_cb.borrow() {
cb(instrument.clone());
}
Ok(())
}
}
impl NavigatorScreen for InstrumentEditor {
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,18 +0,0 @@
pub mod ensemble;
pub use ensemble::*;
pub mod instrument;
pub use instrument::*;
pub mod person;
pub use person::*;
pub mod recording;
pub use recording::*;
pub mod work;
pub use work::*;
mod performance;
mod work_part;
mod work_section;

View file

@ -1,210 +0,0 @@
use crate::backend::Backend;
use crate::database::*;
use crate::selectors::{EnsembleSelector, InstrumentSelector, PersonSelector};
use crate::widgets::{Navigator, NavigatorScreen};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing a performance within a recording.
pub struct PerformanceEditor {
backend: Rc<Backend>,
widget: gtk::Box,
save_button: gtk::Button,
person_label: gtk::Label,
ensemble_label: gtk::Label,
role_label: gtk::Label,
reset_role_button: gtk::Button,
person: RefCell<Option<Person>>,
ensemble: RefCell<Option<Ensemble>>,
role: RefCell<Option<Instrument>>,
selected_cb: RefCell<Option<Box<dyn Fn(Performance) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl PerformanceEditor {
/// Create a new performance editor.
pub fn new(backend: Rc<Backend>, performance: Option<Performance>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/performance_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Button, person_button);
get_widget!(builder, gtk::Button, ensemble_button);
get_widget!(builder, gtk::Button, role_button);
get_widget!(builder, gtk::Button, reset_role_button);
get_widget!(builder, gtk::Label, person_label);
get_widget!(builder, gtk::Label, ensemble_label);
get_widget!(builder, gtk::Label, role_label);
let this = Rc::new(PerformanceEditor {
backend,
widget,
save_button,
person_label,
ensemble_label,
role_label,
reset_role_button,
person: RefCell::new(None),
ensemble: RefCell::new(None),
role: RefCell::new(None),
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(Performance {
person: this.person.borrow().clone(),
ensemble: this.ensemble.borrow().clone(),
role: this.role.borrow().clone(),
});
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
person_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = PersonSelector::new(this.backend.clone());
selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
this.show_person(Some(&person));
this.person.replace(Some(person.clone()));
this.show_ensemble(None);
this.ensemble.replace(None);
navigator.clone().pop();
}));
navigator.push(selector);
}
}));
ensemble_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = EnsembleSelector::new(this.backend.clone());
selector.set_selected_cb(clone!(@strong this, @strong navigator => move |ensemble| {
this.show_person(None);
this.person.replace(None);
this.show_ensemble(Some(&ensemble));
this.ensemble.replace(Some(ensemble.clone()));
navigator.clone().pop();
}));
navigator.push(selector);
}
}));
role_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = InstrumentSelector::new(this.backend.clone());
selector.set_selected_cb(clone!(@strong this, @strong navigator => move |role| {
this.show_role(Some(&role));
this.role.replace(Some(role.clone()));
navigator.clone().pop();
}));
navigator.push(selector);
}
}));
this.reset_role_button
.connect_clicked(clone!(@strong this => move |_| {
this.show_role(None);
this.role.replace(None);
}));
// Initialize
if let Some(performance) = performance {
if let Some(person) = performance.person {
this.show_person(Some(&person));
this.person.replace(Some(person));
} else if let Some(ensemble) = performance.ensemble {
this.show_ensemble(Some(&ensemble));
this.ensemble.replace(Some(ensemble));
}
if let Some(role) = performance.role {
this.show_role(Some(&role));
this.role.replace(Some(role));
}
}
this
}
/// Set a closure to be called when the user has chosen to save the performance.
pub fn set_selected_cb<F: Fn(Performance) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to person.
fn show_person(&self, person: Option<&Person>) {
if let Some(person) = person {
self.person_label.set_text(&person.name_fl());
self.save_button.set_sensitive(true);
} else {
self.person_label.set_text(&gettext("Select …"));
}
}
/// Update the UI according to ensemble.
fn show_ensemble(&self, ensemble: Option<&Ensemble>) {
if let Some(ensemble) = ensemble {
self.ensemble_label.set_text(&ensemble.name);
self.save_button.set_sensitive(true);
} else {
self.ensemble_label.set_text(&gettext("Select …"));
}
}
/// Update the UI according to role.
fn show_role(&self, role: Option<&Instrument>) {
if let Some(role) = role {
self.role_label.set_text(&role.name);
self.reset_role_button.show();
} else {
self.role_label.set_text(&gettext("Select …"));
self.reset_role_button.hide();
}
}
}
impl NavigatorScreen for PerformanceEditor {
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,138 +0,0 @@
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::{Navigator, NavigatorScreen};
use anyhow::Result;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a person.
pub struct PersonEditor {
backend: Rc<Backend>,
id: String,
widget: gtk::Stack,
info_bar: gtk::InfoBar,
first_name_entry: gtk::Entry,
last_name_entry: gtk::Entry,
upload_switch: gtk::Switch,
saved_cb: RefCell<Option<Box<dyn Fn(Person) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl PersonEditor {
/// Create a new person editor and optionally initialize it.
pub fn new(backend: Rc<Backend>, person: Option<Person>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Entry, first_name_entry);
get_widget!(builder, gtk::Entry, last_name_entry);
get_widget!(builder, gtk::Switch, upload_switch);
let id = match person {
Some(person) => {
first_name_entry.set_text(&person.first_name);
last_name_entry.set_text(&person.last_name);
person.id
}
None => generate_id(),
};
let this = Rc::new(Self {
backend,
id,
widget,
info_bar,
first_name_entry,
last_name_entry,
upload_switch,
saved_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
save_button.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.widget.set_visible_child_name("loading");
match clone.clone().save().await {
Ok(_) => {
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.widget.set_visible_child_name("content");
}
}
});
}));
this
}
/// Set the closure to be called if the person was saved.
pub fn set_saved_cb<F: Fn(Person) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}
/// Save the person and possibly upload it to the server.
async fn save(self: Rc<Self>) -> Result<()> {
let first_name = self.first_name_entry.get_text().to_string();
let last_name = self.last_name_entry.get_text().to_string();
let person = Person {
id: self.id.clone(),
first_name,
last_name,
};
let upload = self.upload_switch.get_active();
if upload {
self.backend.post_person(&person).await?;
}
self.backend.db().update_person(person.clone()).await?;
self.backend.library_changed();
if let Some(cb) = &*self.saved_cb.borrow() {
cb(person.clone());
}
Ok(())
}
}
impl NavigatorScreen for PersonEditor {
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,276 +0,0 @@
use super::performance::PerformanceEditor;
use crate::backend::Backend;
use crate::database::*;
use crate::selectors::{PersonSelector, WorkSelector};
use crate::widgets::{List, Navigator, NavigatorScreen};
use anyhow::Result;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A widget for creating or editing a recording.
// TODO: Disable buttons if no performance is selected.
pub struct RecordingEditor {
pub widget: gtk::Stack,
backend: Rc<Backend>,
save_button: gtk::Button,
info_bar: gtk::InfoBar,
work_label: gtk::Label,
comment_entry: gtk::Entry,
upload_switch: gtk::Switch,
performance_list: Rc<List<Performance>>,
id: String,
work: RefCell<Option<Work>>,
performances: RefCell<Vec<Performance>>,
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingEditor {
/// Create a new recording editor widget and optionally initialize it.
pub fn new(backend: Rc<Backend>, recording: Option<Recording>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, work_button);
get_widget!(builder, gtk::Label, work_label);
get_widget!(builder, gtk::Entry, comment_entry);
get_widget!(builder, gtk::Switch, upload_switch);
get_widget!(builder, gtk::ScrolledWindow, scroll);
get_widget!(builder, gtk::Button, add_performer_button);
get_widget!(builder, gtk::Button, edit_performer_button);
get_widget!(builder, gtk::Button, remove_performer_button);
let performance_list = List::new(&gettext("No performers added."));
scroll.add(&performance_list.widget);
let (id, work, performances) = match recording {
Some(recording) => {
comment_entry.set_text(&recording.comment);
(recording.id, Some(recording.work), recording.performances)
}
None => (generate_id(), None, Vec::new()),
};
let this = Rc::new(RecordingEditor {
widget,
backend,
save_button,
info_bar,
work_label,
comment_entry,
upload_switch,
performance_list,
id,
work: RefCell::new(work),
performances: RefCell::new(performances),
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.widget.set_visible_child_name("loading");
match clone.clone().save().await {
Ok(_) => {
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.widget.set_visible_child_name("content");
}
}
});
}));
work_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let person_selector = PersonSelector::new(this.backend.clone());
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
this.work_selected(&work);
this.work.replace(Some(work.clone()));
navigator.clone().pop();
navigator.clone().pop();
}));
navigator.clone().push(work_selector);
}));
navigator.push(person_selector);
}
}));
this.performance_list.set_make_widget(|performance| {
let label = gtk::Label::new(Some(&performance.get_title()));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
add_performer_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = PerformanceEditor::new(this.backend.clone(), None);
editor.set_selected_cb(clone!(@strong this, @strong navigator => move |performance| {
let mut performances = this.performances.borrow_mut();
let index = match this.performance_list.get_selected_index() {
Some(index) => index + 1,
None => performances.len(),
};
performances.insert(index, performance);
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
navigator.clone().pop();
}));
navigator.push(editor);
}
}));
edit_performer_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
if let Some(index) = this.performance_list.get_selected_index() {
let performance = &this.performances.borrow()[index];
let editor = PerformanceEditor::new(
this.backend.clone(),
Some(performance.clone()),
);
editor.set_selected_cb(clone!(@strong this, @strong navigator => move |performance| {
let mut performances = this.performances.borrow_mut();
performances[index] = performance;
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
navigator.clone().pop();
}));
navigator.push(editor);
}
}
}));
remove_performer_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.performance_list.get_selected_index() {
let mut performances = this.performances.borrow_mut();
performances.remove(index);
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
}
}));
// Initialize
if let Some(work) = &*this.work.borrow() {
this.work_selected(work);
}
this.performance_list
.show_items(this.performances.borrow().clone());
this
}
/// Set the closure to be called if the recording was created.
pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to work.
fn work_selected(&self, work: &Work) {
self.work_label
.set_text(&format!("{}: {}", work.composer.name_fl(), work.title));
self.save_button.set_sensitive(true);
}
/// Save the recording and possibly upload it to the server.
async fn save(self: Rc<Self>) -> Result<()> {
let recording = Recording {
id: self.id.clone(),
work: self
.work
.borrow()
.clone()
.expect("Tried to create recording without work!"),
comment: self.comment_entry.get_text().to_string(),
performances: self.performances.borrow().clone(),
};
let upload = self.upload_switch.get_active();
if upload {
self.backend.post_recording(&recording).await?;
}
self.backend
.db()
.update_recording(recording.clone().into())
.await
.unwrap();
self.backend.library_changed();
if let Some(cb) = &*self.selected_cb.borrow() {
cb(recording.clone());
}
let navigator = self.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
Ok(())
}
}
impl NavigatorScreen for RecordingEditor {
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,432 +0,0 @@
use super::work_part::WorkPartEditor;
use super::work_section::WorkSectionEditor;
use crate::backend::Backend;
use crate::database::*;
use crate::selectors::{InstrumentSelector, PersonSelector};
use crate::widgets::{List, Navigator, NavigatorScreen};
use anyhow::Result;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::convert::TryInto;
use std::rc::Rc;
/// Either a work part or a work section.
#[derive(Clone)]
enum PartOrSection {
Part(WorkPart),
Section(WorkSection),
}
/// A widget for editing and creating works.
pub struct WorkEditor {
widget: gtk::Stack,
backend: Rc<Backend>,
save_button: gtk::Button,
title_entry: gtk::Entry,
info_bar: gtk::InfoBar,
composer_label: gtk::Label,
upload_switch: gtk::Switch,
instrument_list: Rc<List<Instrument>>,
part_list: Rc<List<PartOrSection>>,
id: String,
composer: RefCell<Option<Person>>,
instruments: RefCell<Vec<Instrument>>,
structure: RefCell<Vec<PartOrSection>>,
saved_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl WorkEditor {
/// Create a new work editor widget and optionally initialize it.
pub fn new(backend: Rc<Backend>, work: Option<Work>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Entry, title_entry);
get_widget!(builder, gtk::Button, composer_button);
get_widget!(builder, gtk::Label, composer_label);
get_widget!(builder, gtk::Switch, upload_switch);
get_widget!(builder, gtk::ScrolledWindow, instruments_scroll);
get_widget!(builder, gtk::Button, add_instrument_button);
get_widget!(builder, gtk::Button, remove_instrument_button);
get_widget!(builder, gtk::ScrolledWindow, structure_scroll);
get_widget!(builder, gtk::Button, add_part_button);
get_widget!(builder, gtk::Button, remove_part_button);
get_widget!(builder, gtk::Button, add_section_button);
get_widget!(builder, gtk::Button, edit_part_button);
get_widget!(builder, gtk::Button, move_part_up_button);
get_widget!(builder, gtk::Button, move_part_down_button);
let instrument_list = List::new(&gettext("No instruments added."));
instruments_scroll.add(&instrument_list.widget);
let part_list = List::new(&gettext("No work parts added."));
structure_scroll.add(&part_list.widget);
let (id, composer, instruments, structure) = match work {
Some(work) => {
title_entry.set_text(&work.title);
let mut structure = Vec::new();
for part in work.parts {
structure.push(PartOrSection::Part(part));
}
for section in work.sections {
structure.insert(
section.before_index.try_into().unwrap(),
PartOrSection::Section(section),
);
}
(work.id, Some(work.composer), work.instruments, structure)
}
None => (generate_id(), None, Vec::new(), Vec::new()),
};
let this = Rc::new(Self {
widget,
backend,
save_button,
id,
info_bar,
title_entry,
composer_label,
upload_switch,
instrument_list,
part_list,
composer: RefCell::new(composer),
instruments: RefCell::new(instruments),
structure: RefCell::new(structure),
saved_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.widget.set_visible_child_name("loading");
match clone.clone().save().await {
Ok(_) => {
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.widget.set_visible_child_name("content");
}
}
});
}));
composer_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = PersonSelector::new(this.backend.clone());
selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
this.show_composer(person);
this.composer.replace(Some(person.clone()));
navigator.clone().pop();
}));
navigator.push(selector);
}
}));
this.instrument_list.set_make_widget(|instrument| {
let label = gtk::Label::new(Some(&instrument.name));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
add_instrument_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = InstrumentSelector::new(this.backend.clone());
selector.set_selected_cb(clone!(@strong this, @strong navigator => move |instrument| {
let mut instruments = this.instruments.borrow_mut();
let index = match this.instrument_list.get_selected_index() {
Some(index) => index + 1,
None => instruments.len(),
};
instruments.insert(index, instrument.clone());
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
navigator.clone().pop();
}));
navigator.push(selector);
}
}));
remove_instrument_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.instrument_list.get_selected_index() {
let mut instruments = this.instruments.borrow_mut();
instruments.remove(index);
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
}
}));
this.part_list.set_make_widget(|pos| {
let label = gtk::Label::new(None);
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
match pos {
PartOrSection::Part(part) => {
label.set_text(&part.title);
label.set_margin_start(12);
}
PartOrSection::Section(section) => {
let attrs = pango::AttrList::new();
attrs.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap());
label.set_attributes(Some(&attrs));
label.set_text(&section.title);
label.set_margin_start(6);
}
}
label.upcast()
});
add_part_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = WorkPartEditor::new(this.backend.clone(), None);
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |part| {
let mut structure = this.structure.borrow_mut();
let index = match this.part_list.get_selected_index() {
Some(index) => index + 1,
None => structure.len(),
};
structure.insert(index, PartOrSection::Part(part));
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
navigator.clone().pop();
}));
navigator.push(editor);
}
}));
add_section_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = WorkSectionEditor::new(None);
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |section| {
let mut structure = this.structure.borrow_mut();
let index = match this.part_list.get_selected_index() {
Some(index) => index + 1,
None => structure.len(),
};
structure.insert(index, PartOrSection::Section(section));
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
navigator.clone().pop();
}));
navigator.push(editor);
}
}));
edit_part_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
if let Some(index) = this.part_list.get_selected_index() {
match this.structure.borrow()[index].clone() {
PartOrSection::Part(part) => {
let editor = WorkPartEditor::new(this.backend.clone(), Some(part));
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |part| {
let mut structure = this.structure.borrow_mut();
structure[index] = PartOrSection::Part(part);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
navigator.clone().pop();
}));
navigator.push(editor);
}
PartOrSection::Section(section) => {
let editor = WorkSectionEditor::new(Some(section));
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |section| {
let mut structure = this.structure.borrow_mut();
structure[index] = PartOrSection::Section(section);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
navigator.clone().pop();
}));
navigator.push(editor);
}
}
}
}
}));
remove_part_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.part_list.get_selected_index() {
let mut structure = this.structure.borrow_mut();
structure.remove(index);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
}
}));
move_part_up_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.part_list.get_selected_index() {
if index > 0 {
let mut structure = this.structure.borrow_mut();
structure.swap(index - 1, index);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index - 1);
}
}
}));
move_part_down_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.part_list.get_selected_index() {
let mut structure = this.structure.borrow_mut();
if index < structure.len() - 1 {
structure.swap(index, index + 1);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index + 1);
}
}
}));
// Initialization
if let Some(composer) = &*this.composer.borrow() {
this.show_composer(composer);
}
this.instrument_list
.show_items(this.instruments.borrow().clone());
this.part_list.show_items(this.structure.borrow().clone());
this
}
/// The closure to call when a work was created.
pub fn set_saved_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to person.
fn show_composer(&self, person: &Person) {
self.composer_label.set_text(&person.name_fl());
self.save_button.set_sensitive(true);
}
/// Save the work and possibly upload it to the server.
async fn save(self: Rc<Self>) -> Result<()> {
let mut section_count: usize = 0;
let mut parts = Vec::new();
let mut sections = Vec::new();
for (index, pos) in self.structure.borrow().iter().enumerate() {
match pos {
PartOrSection::Part(part) => parts.push(part.clone()),
PartOrSection::Section(section) => {
let mut section = section.clone();
section.before_index = index - section_count;
sections.push(section);
section_count += 1;
}
}
}
let work = Work {
id: self.id.clone(),
title: self.title_entry.get_text().to_string(),
composer: self
.composer
.borrow()
.clone()
.expect("Tried to create work without composer!"),
instruments: self.instruments.borrow().clone(),
parts: parts,
sections: sections,
};
let upload = self.upload_switch.get_active();
if upload {
self.backend.post_work(&work).await?;
}
self.backend
.db()
.update_work(work.clone().into())
.await
.unwrap();
self.backend.library_changed();
if let Some(cb) = &*self.saved_cb.borrow() {
cb(work.clone());
}
Ok(())
}
}
impl NavigatorScreen for WorkEditor {
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,141 +0,0 @@
use crate::backend::Backend;
use crate::database::*;
use crate::selectors::PersonSelector;
use crate::widgets::{Navigator, NavigatorScreen};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a work part.
pub struct WorkPartEditor {
backend: Rc<Backend>,
widget: gtk::Box,
title_entry: gtk::Entry,
composer_label: gtk::Label,
reset_composer_button: gtk::Button,
composer: RefCell<Option<Person>>,
ready_cb: RefCell<Option<Box<dyn Fn(WorkPart) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl WorkPartEditor {
/// Create a new part editor and optionally initialize it.
pub fn new(backend: Rc<Backend>, part: Option<WorkPart>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_part_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, title_entry);
get_widget!(builder, gtk::Button, composer_button);
get_widget!(builder, gtk::Label, composer_label);
get_widget!(builder, gtk::Button, reset_composer_button);
let composer = match part {
Some(part) => {
title_entry.set_text(&part.title);
part.composer
}
None => None,
};
let this = Rc::new(Self {
backend,
widget,
title_entry,
composer_label,
reset_composer_button,
composer: RefCell::new(composer),
ready_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.ready_cb.borrow() {
cb(WorkPart {
title: this.title_entry.get_text().to_string(),
composer: this.composer.borrow().clone(),
});
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
composer_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = PersonSelector::new(this.backend.clone());
selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
this.show_composer(Some(person));
this.composer.replace(Some(person.clone()));
navigator.clone().pop();
}));
navigator.push(selector);
}
}));
this.reset_composer_button
.connect_clicked(clone!(@strong this => move |_| {
this.composer.replace(None);
this.show_composer(None);
}));
// Initialize
if let Some(composer) = &*this.composer.borrow() {
this.show_composer(Some(composer));
}
this
}
/// Set the closure to be called when the user wants to save the part.
pub fn set_ready_cb<F: Fn(WorkPart) -> () + 'static>(&self, cb: F) {
self.ready_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to person.
fn show_composer(&self, person: Option<&Person>) {
if let Some(person) = person {
self.composer_label.set_text(&person.name_fl());
self.reset_composer_button.show();
} else {
self.composer_label.set_text(&gettext("Select …"));
self.reset_composer_button.hide();
}
}
}
impl NavigatorScreen for WorkPartEditor {
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,86 +0,0 @@
use crate::database::*;
use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a work section.
pub struct WorkSectionEditor {
widget: gtk::Box,
title_entry: gtk::Entry,
ready_cb: RefCell<Option<Box<dyn Fn(WorkSection) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl WorkSectionEditor {
/// Create a new section editor and optionally initialize it.
pub fn new(section: Option<WorkSection>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_section_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, title_entry);
if let Some(section) = section {
title_entry.set_text(&section.title);
}
let this = Rc::new(Self {
widget,
title_entry,
ready_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.ready_cb.borrow() {
cb(WorkSection {
before_index: 0,
title: this.title_entry.get_text().to_string(),
});
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this
}
/// Set the closure to be called when the user wants to save the section. Note that the
/// resulting object will always have `before_index` set to 0. The caller is expected to
/// change that later before adding the section to the database.
pub fn set_ready_cb<F: Fn(WorkSection) -> () + 'static>(&self, cb: F) {
self.ready_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for WorkSectionEditor {
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,175 +0,0 @@
use anyhow::{anyhow, bail, Result};
use discid::DiscId;
use futures_channel::oneshot;
use gstreamer::prelude::*;
use gstreamer::{Element, ElementFactory, Pipeline};
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::thread;
/// Representation of an audio CD being imported as a medium.
#[derive(Clone, Debug)]
pub struct DiscSource {
/// The MusicBrainz DiscID of the CD.
pub discid: String,
/// The path to the temporary directory where the audio files will be.
pub path: PathBuf,
/// The tracks on this disc.
pub tracks: Vec<TrackSource>,
}
/// Representation of a single track on an audio CD.
#[derive(Clone, Debug)]
pub struct TrackSource {
/// The track number. This is different from the index in the disc
/// source's tracks list, because it is not defined from which number the
/// the track numbers start.
pub number: u32,
/// The path to the temporary file to which the track will be ripped. The
/// file will not exist until the track is actually ripped.
pub path: PathBuf,
}
impl DiscSource {
/// Try to create a new disc source by asynchronously reading the
/// information from the default disc drive.
pub async fn load() -> Result<Self> {
let (sender, receiver) = oneshot::channel();
thread::spawn(|| {
let disc = Self::load_priv();
sender.send(disc).unwrap();
});
let disc = receiver.await??;
Ok(disc)
}
/// Rip the whole disc asynchronously. After this method has finished
/// successfully, the audio files will be available in the specified
/// location for each track source.
pub async fn rip(&self) -> Result<()> {
for track in &self.tracks {
let (sender, receiver) = oneshot::channel();
let number = track.number;
let path = track.path.clone();
thread::spawn(move || {
let result = Self::rip_track(&path, number);
sender.send(result).unwrap();
});
receiver.await??;
}
Ok(())
}
/// Load the disc from the default disc drive.
fn load_priv() -> Result<Self> {
let discid = DiscId::read(None)?;
let id = discid.id();
let mut tracks = Vec::new();
let first_track = discid.first_track_num() as u32;
let last_track = discid.last_track_num() as u32;
let tmp_dir = Self::create_tmp_dir()?;
for number in first_track..=last_track {
let file_name = format!("track_{:02}.flac", number);
let mut path = tmp_dir.clone();
path.push(file_name);
let track = TrackSource {
number,
path,
};
tracks.push(track);
}
let disc = DiscSource {
discid: id,
tracks,
path: tmp_dir,
};
Ok(disc)
}
/// Create a new temporary directory and return its path.
// TODO: Move to a more appropriate place.
fn create_tmp_dir() -> Result<PathBuf> {
let mut tmp_dir = glib::get_tmp_dir()
.ok_or_else(|| {
anyhow!("Failed to get temporary directory using glib::get_tmp_dir()!")
})?;
let dir_name = format!("musicus-{}", rand::random::<u64>());
tmp_dir.push(dir_name);
std::fs::create_dir(&tmp_dir)?;
Ok(tmp_dir)
}
/// Rip one track.
fn rip_track(path: &Path, number: u32) -> Result<()> {
let pipeline = Self::build_pipeline(path, number)?;
let bus = pipeline
.get_bus()
.ok_or_else(|| anyhow!("Failed to get bus from pipeline!"))?;
pipeline.set_state(gstreamer::State::Playing)?;
for msg in bus.iter_timed(gstreamer::CLOCK_TIME_NONE) {
use gstreamer::MessageView::*;
match msg.view() {
Eos(..) => break,
Error(err) => {
pipeline.set_state(gstreamer::State::Null)?;
bail!("GStreamer error: {:?}!", err);
}
_ => (),
}
}
pipeline.set_state(gstreamer::State::Null)?;
Ok(())
}
/// Build the GStreamer pipeline to rip a track.
fn build_pipeline(path: &Path, number: u32) -> Result<Pipeline> {
let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?;
cdparanoiasrc.set_property("track", &number)?;
let queue = ElementFactory::make("queue", None)?;
let audioconvert = ElementFactory::make("audioconvert", None)?;
let flacenc = ElementFactory::make("flacenc", None)?;
let path_str = path.to_str().ok_or_else(|| {
anyhow!("Failed to convert path '{:?}' to string!", path)
})?;
let filesink = gstreamer::ElementFactory::make("filesink", None)?;
filesink.set_property("location", &path_str.to_owned())?;
let pipeline = gstreamer::Pipeline::new(None);
pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Ok(pipeline)
}
}

View file

@ -1,241 +0,0 @@
use super::disc_source::DiscSource;
use super::track_set_editor::{TrackSetData, TrackSetEditor};
use crate::database::{generate_id, Medium, Track, TrackSet};
use crate::backend::Backend;
use crate::widgets::{Navigator, NavigatorScreen};
use crate::widgets::new_list::List;
use anyhow::Result;
use glib::clone;
use glib::prelude::*;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing metadata while importing music into the music library.
pub struct MediumEditor {
backend: Rc<Backend>,
source: Rc<DiscSource>,
widget: gtk::Stack,
done_button: gtk::Button,
done_stack: gtk::Stack,
done: gtk::Image,
name_entry: gtk::Entry,
publish_switch: gtk::Switch,
track_set_list: List,
track_sets: RefCell<Vec<TrackSetData>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl MediumEditor {
/// Create a new medium editor.
pub fn new(backend: Rc<Backend>, source: DiscSource) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, done_button);
get_widget!(builder, gtk::Stack, done_stack);
get_widget!(builder, gtk::Image, done);
get_widget!(builder, gtk::Entry, name_entry);
get_widget!(builder, gtk::Switch, publish_switch);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Frame, frame);
let list = List::new("No recordings added.");
frame.add(&list.widget);
let this = Rc::new(Self {
backend,
source: Rc::new(source),
widget,
done_button,
done_stack,
done,
name_entry,
publish_switch,
track_set_list: list,
track_sets: RefCell::new(Vec::new()),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.done_button.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.widget.set_visible_child_name("loading");
match clone.clone().save().await {
Ok(_) => (),
Err(err) => {
println!("{:?}", err);
// clone.info_bar.set_revealed(true);
}
}
});
}));
add_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = TrackSetEditor::new(this.backend.clone(), Rc::clone(&this.source));
editor.set_done_cb(clone!(@strong this => move |track_set| {
let length = {
let mut track_sets = this.track_sets.borrow_mut();
track_sets.push(track_set);
track_sets.len()
};
this.track_set_list.update(length);
}));
navigator.push(editor);
}
}));
this.track_set_list.set_make_widget(clone!(@strong this => move |index| {
let track_set = &this.track_sets.borrow()[index];
let title = track_set.recording.work.get_title();
let subtitle = track_set.recording.get_performers();
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
let edit_button = gtk::Button::new();
edit_button.set_relief(gtk::ReliefStyle::None);
edit_button.set_valign(gtk::Align::Center);
edit_button.add(&edit_image);
let row = libhandy::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&title));
row.set_subtitle(Some(&subtitle));
row.add(&edit_button);
row.set_activatable_widget(Some(&edit_button));
row.show_all();
edit_button.connect_clicked(clone!(@strong this => move |_| {
}));
row.upcast()
}));
// Start ripping the CD in the background.
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
match clone.source.rip().await {
Err(error) => {
// TODO: Present error.
println!("Failed to rip: {}", error);
},
Ok(_) => {
clone.done_stack.set_visible_child(&clone.done);
clone.done_button.set_sensitive(true);
}
}
});
this
}
/// Save the medium and possibly upload it to the server.
async fn save(self: Rc<Self>) -> Result<()> {
let name = self.name_entry.get_text().to_string();
// Create a new directory in the music library path for the imported medium.
let mut path = self.backend.get_music_library_path().unwrap().clone();
path.push(&name);
std::fs::create_dir(&path)?;
// Convert the track set data to real track sets.
let mut track_sets = Vec::new();
for track_set_data in &*self.track_sets.borrow() {
let mut tracks = Vec::new();
for track_data in &track_set_data.tracks {
// Copy the corresponding audio file to the music library.
let track_source = &self.source.tracks[track_data.track_source];
let file_name = format!("track_{:02}.flac", track_source.number);
let mut track_path = path.clone();
track_path.push(&file_name);
std::fs::copy(&track_source.path, &track_path)?;
// Create the real track.
let track = Track {
work_parts: track_data.work_parts.clone(),
path: track_path.to_str().unwrap().to_owned(),
};
tracks.push(track);
}
let track_set = TrackSet {
recording: track_set_data.recording.clone(),
tracks,
};
track_sets.push(track_set);
}
let medium = Medium {
id: generate_id(),
name: self.name_entry.get_text().to_string(),
discid: Some(self.source.discid.clone()),
tracks: track_sets,
};
let upload = self.publish_switch.get_active();
if upload {
// self.backend.post_medium(&medium).await?;
}
self.backend
.db()
.update_medium(medium.clone())
.await?;
self.backend.library_changed();
let navigator = self.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
Ok(())
}
}
impl NavigatorScreen for MediumEditor {
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,8 +0,0 @@
mod disc_source;
mod medium_editor;
mod source_selector;
mod track_editor;
mod track_selector;
mod track_set_editor;
pub use source_selector::SourceSelector;

View file

@ -1,92 +0,0 @@
use super::medium_editor::MediumEditor;
use super::disc_source::DiscSource;
use crate::backend::Backend;
use crate::widgets::{Navigator, NavigatorScreen};
use anyhow::Result;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for starting to import music.
pub struct SourceSelector {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
info_bar: gtk::InfoBar,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl SourceSelector {
/// Create a new source selector.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, import_button);
let this = Rc::new(Self {
backend,
widget,
stack,
info_bar,
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
import_button.connect_clicked(clone!(@strong this => move |_| {
this.stack.set_visible_child_name("loading");
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
match DiscSource::load().await {
Ok(disc) => {
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = MediumEditor::new(clone.backend.clone(), disc);
navigator.push(editor);
}
clone.info_bar.set_revealed(false);
clone.stack.set_visible_child_name("start");
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.stack.set_visible_child_name("start");
}
}
});
}));
this
}
}
impl NavigatorScreen for SourceSelector {
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,110 +0,0 @@
use crate::database::Recording;
use crate::widgets::{Navigator, NavigatorScreen};
use crate::widgets::new_list::List;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for editing a single track.
pub struct TrackEditor {
widget: gtk::Box,
selection: RefCell<Vec<usize>>,
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TrackEditor {
/// Create a new track editor.
pub fn new(recording: Recording, selection: Vec<usize>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, select_button);
get_widget!(builder, gtk::Frame, parts_frame);
let parts_list = gtk::ListBox::new();
parts_list.set_selection_mode(gtk::SelectionMode::None);
parts_list.set_vexpand(false);
parts_list.show();
parts_frame.add(&parts_list);
let this = Rc::new(Self {
widget,
selection: RefCell::new(selection),
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
select_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
if let Some(cb) = &*this.selected_cb.borrow() {
let selection = this.selection.borrow().clone();
cb(selection);
}
}));
for (index, part) in recording.work.parts.iter().enumerate() {
let check = gtk::CheckButton::new();
check.set_active(this.selection.borrow().contains(&index));
check.connect_toggled(clone!(@strong this => move |check| {
let mut selection = this.selection.borrow_mut();
if check.get_active() {
selection.push(index);
} else {
if let Some(pos) = selection.iter().position(|part| *part == index) {
selection.remove(pos);
}
}
}));
let row = libhandy::ActionRow::new();
row.add_prefix(&check);
row.set_activatable_widget(Some(&check));
row.set_title(Some(&part.title));
row.show_all();
parts_list.add(&row);
}
this
}
/// Set the closure to be called when the user has edited the track.
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for TrackEditor {
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,122 +0,0 @@
use super::disc_source::DiscSource;
use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting tracks from a medium.
pub struct TrackSelector {
source: Rc<DiscSource>,
widget: gtk::Box,
select_button: gtk::Button,
selection: RefCell<Vec<usize>>,
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TrackSelector {
/// Create a new track selector.
pub fn new(source: Rc<DiscSource>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, select_button);
get_widget!(builder, gtk::Frame, tracks_frame);
let track_list = gtk::ListBox::new();
track_list.set_selection_mode(gtk::SelectionMode::None);
track_list.set_vexpand(false);
track_list.show();
tracks_frame.add(&track_list);
let this = Rc::new(Self {
source,
widget,
select_button,
selection: RefCell::new(Vec::new()),
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.select_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
if let Some(cb) = &*this.selected_cb.borrow() {
let selection = this.selection.borrow().clone();
cb(selection);
}
}));
for (index, track) in this.source.tracks.iter().enumerate() {
let check = gtk::CheckButton::new();
check.connect_toggled(clone!(@strong this => move |check| {
let mut selection = this.selection.borrow_mut();
if check.get_active() {
selection.push(index);
} else {
if let Some(pos) = selection.iter().position(|part| *part == index) {
selection.remove(pos);
}
}
if selection.is_empty() {
this.select_button.set_sensitive(false);
} else {
this.select_button.set_sensitive(true);
}
}));
let title = format!("Track {}", track.number);
let row = libhandy::ActionRow::new();
row.add_prefix(&check);
row.set_activatable_widget(Some(&check));
row.set_title(Some(&title));
row.show_all();
track_list.add(&row);
}
this
}
/// Set the closure to be called when the user has selected tracks. The
/// closure will be called with the indices of the selected tracks as its
/// argument.
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for TrackSelector {
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,283 +0,0 @@
use super::disc_source::DiscSource;
use super::track_editor::TrackEditor;
use super::track_selector::TrackSelector;
use crate::backend::Backend;
use crate::database::{Recording, Track, TrackSet};
use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector};
use crate::widgets::{Navigator, NavigatorScreen};
use crate::widgets::new_list::List;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::rc::Rc;
/// A track set before being imported.
#[derive(Clone, Debug)]
pub struct TrackSetData {
pub recording: Recording,
pub tracks: Vec<TrackData>,
}
/// A track before being imported.
#[derive(Clone, Debug)]
pub struct TrackData {
/// Index of the track source within the medium source's tracks.
pub track_source: usize,
/// Actual track data.
pub work_parts: Vec<usize>,
}
/// A screen for editing a set of tracks for one recording.
pub struct TrackSetEditor {
backend: Rc<Backend>,
source: Rc<DiscSource>,
widget: gtk::Box,
save_button: gtk::Button,
recording_row: libhandy::ActionRow,
track_list: List,
recording: RefCell<Option<Recording>>,
tracks: RefCell<Vec<TrackData>>,
done_cb: RefCell<Option<Box<dyn Fn(TrackSetData)>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TrackSetEditor {
/// Create a new track set editor.
pub fn new(backend: Rc<Backend>, source: Rc<DiscSource>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, libhandy::ActionRow, recording_row);
get_widget!(builder, gtk::Button, select_recording_button);
get_widget!(builder, gtk::Button, edit_tracks_button);
get_widget!(builder, gtk::Frame, tracks_frame);
let track_list = List::new(&gettext!("No tracks added"));
tracks_frame.add(&track_list.widget);
let this = Rc::new(Self {
backend,
source,
widget,
save_button,
recording_row,
track_list,
recording: RefCell::new(None),
tracks: RefCell::new(Vec::new()),
done_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.done_cb.borrow() {
let data = TrackSetData {
recording: this.recording.borrow().clone().unwrap(),
tracks: this.tracks.borrow().clone(),
};
cb(data);
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
select_recording_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let person_selector = PersonSelector::new(this.backend.clone());
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone());
recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| {
this.recording.replace(Some(recording.clone()));
this.recording_selected();
navigator.clone().pop();
navigator.clone().pop();
navigator.clone().pop();
}));
navigator.clone().push(recording_selector);
}));
navigator.clone().push(work_selector);
}));
navigator.clone().push(person_selector);
}
}));
edit_tracks_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = TrackSelector::new(Rc::clone(&this.source));
selector.set_selected_cb(clone!(@strong this => move |selection| {
let mut tracks = Vec::new();
for index in selection {
let data = TrackData {
track_source: index,
work_parts: Vec::new(),
};
tracks.push(data);
}
let length = tracks.len();
this.tracks.replace(tracks);
this.track_list.update(length);
this.autofill_parts();
}));
navigator.push(selector);
}
}));
this.track_list.set_make_widget(clone!(@strong this => move |index| {
let track = &this.tracks.borrow()[index];
let mut title_parts = Vec::<String>::new();
if let Some(recording) = &*this.recording.borrow() {
for part in &track.work_parts {
title_parts.push(recording.work.parts[*part].title.clone());
}
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let number = this.source.tracks[track.track_source].number;
let subtitle = format!("Track {}", number);
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
let edit_button = gtk::Button::new();
edit_button.set_relief(gtk::ReliefStyle::None);
edit_button.set_valign(gtk::Align::Center);
edit_button.add(&edit_image);
let row = libhandy::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&title));
row.set_subtitle(Some(&subtitle));
row.add(&edit_button);
row.set_activatable_widget(Some(&edit_button));
row.show_all();
edit_button.connect_clicked(clone!(@strong this => move |_| {
let recording = this.recording.borrow().clone();
let navigator = this.navigator.borrow().clone();
if let (Some(recording), Some(navigator)) = (recording, navigator) {
let track = &this.tracks.borrow()[index];
let editor = TrackEditor::new(recording, track.work_parts.clone());
editor.set_selected_cb(clone!(@strong this => move |selection| {
{
let mut tracks = this.tracks.borrow_mut();
let mut track = &mut tracks[index];
track.work_parts = selection;
};
this.update_tracks();
}));
navigator.push(editor);
}
}));
row.upcast()
}));
this
}
/// Set the closure to be called when the user has created the track set.
pub fn set_done_cb<F: Fn(TrackSetData) + 'static>(&self, cb: F) {
self.done_cb.replace(Some(Box::new(cb)));
}
/// Set everything up after selecting a recording.
fn recording_selected(&self) {
if let Some(recording) = &*self.recording.borrow() {
self.recording_row.set_title(Some(&recording.work.get_title()));
self.recording_row.set_subtitle(Some(&recording.get_performers()));
self.save_button.set_sensitive(true);
}
self.autofill_parts();
}
/// Automatically try to put work part information from the selected recording into the
/// selected tracks.
fn autofill_parts(&self) {
if let Some(recording) = &*self.recording.borrow() {
let mut tracks = self.tracks.borrow_mut();
for (index, _) in recording.work.parts.iter().enumerate() {
if let Some(mut track) = tracks.get_mut(index) {
track.work_parts = vec![index];
} else {
break;
}
}
}
self.update_tracks();
}
/// Update the track list.
fn update_tracks(&self) {
let length = self.tracks.borrow().len();
self.track_list.update(length);
}
}
impl NavigatorScreen for TrackSetEditor {
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,55 +0,0 @@
// Required for database/schema.rs
#[macro_use]
extern crate diesel;
// Required for embed_migrations macro in database/database.rs
#[macro_use]
extern crate diesel_migrations;
use gio::prelude::*;
use glib::clone;
use std::cell::RefCell;
use std::rc::Rc;
mod backend;
mod config;
mod database;
mod dialogs;
mod editors;
mod import;
mod player;
mod screens;
mod selectors;
mod widgets;
mod window;
use window::Window;
mod resources;
fn main() {
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
gettextrs::bindtextdomain("musicus", config::LOCALEDIR);
gettextrs::textdomain("musicus");
gstreamer::init().expect("Failed to initialize GStreamer!");
gtk::init().expect("Failed to initialize GTK!");
libhandy::init();
resources::init().expect("Failed to initialize resources!");
let app = gtk::Application::new(Some("de.johrpan.musicus"), gio::ApplicationFlags::empty())
.expect("Failed to initialize GTK application!");
let window: RefCell<Option<Rc<Window>>> = RefCell::new(None);
app.connect_activate(clone!(@strong app => move |_| {
let mut window = window.borrow_mut();
if window.is_none() {
window.replace(Window::new(&app));
}
window.as_ref().unwrap().present();
}));
let args = std::env::args().collect::<Vec<String>>();
app.run(&args);
}

View file

@ -1,115 +0,0 @@
prefix = get_option('prefix')
localedir = join_paths(prefix, get_option('localedir'))
global_conf = configuration_data()
global_conf.set_quoted('LOCALEDIR', localedir)
global_conf.set_quoted('VERSION', meson.project_version())
config_rs = configure_file(
input: 'config.rs.in',
output: 'config.rs',
configuration: global_conf
)
run_command(
'cp',
config_rs,
meson.current_source_dir(),
check: true
)
resource_conf = configuration_data()
resource_conf.set_quoted('RESOURCEFILE', resources.full_path())
resource_rs = configure_file(
input: 'resources.rs.in',
output: 'resources.rs',
configuration: resource_conf
)
run_command(
'cp',
resource_rs,
meson.current_source_dir(),
check: true
)
sources = files(
'backend/client/ensembles.rs',
'backend/client/instruments.rs',
'backend/client/mod.rs',
'backend/client/persons.rs',
'backend/client/recordings.rs',
'backend/client/works.rs',
'backend/library.rs',
'backend/mod.rs',
'backend/secure.rs',
'database/ensembles.rs',
'database/instruments.rs',
'database/medium.rs',
'database/mod.rs',
'database/persons.rs',
'database/recordings.rs',
'database/schema.rs',
'database/thread.rs',
'database/works.rs',
'dialogs/about.rs',
'dialogs/login_dialog.rs',
'dialogs/mod.rs',
'dialogs/preferences.rs',
'dialogs/server_dialog.rs',
'editors/ensemble.rs',
'editors/instrument.rs',
'editors/mod.rs',
'editors/performance.rs',
'editors/person.rs',
'editors/recording.rs',
'editors/work.rs',
'editors/work_part.rs',
'editors/work_section.rs',
'screens/ensemble_screen.rs',
'screens/mod.rs',
'screens/person_screen.rs',
'screens/player_screen.rs',
'screens/recording_screen.rs',
'screens/work_screen.rs',
'selectors/ensemble.rs',
'selectors/instrument.rs',
'selectors/mod.rs',
'selectors/person.rs',
'selectors/recording.rs',
'selectors/selector.rs',
'selectors/work.rs',
'widgets/list.rs',
'widgets/mod.rs',
'widgets/navigator.rs',
'widgets/navigator_window.rs',
'widgets/player_bar.rs',
'widgets/poe_list.rs',
'widgets/selector_row.rs',
'config.rs',
'config.rs.in',
'main.rs',
'player.rs',
'resources.rs',
'resources.rs.in',
'window.rs',
)
cargo_script = find_program(join_paths(meson.source_root(), 'build-aux/cargo.sh'))
cargo_release = custom_target(
'cargo-build',
build_by_default: true,
input: sources,
depends: resources,
output: meson.project_name(),
console: true,
install: true,
install_dir: get_option('bindir'),
command: [
cargo_script,
meson.build_root(),
meson.source_root(),
'@OUTPUT@',
get_option('buildtype'),
meson.project_name(),
]
)

View file

@ -1,287 +0,0 @@
use crate::database::*;
use anyhow::anyhow;
use anyhow::Result;
use gstreamer_player::prelude::*;
use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::rc::Rc;
#[derive(Clone)]
pub struct PlaylistItem {
pub track_set: TrackSet,
pub indices: Vec<usize>,
}
pub struct Player {
music_library_path: PathBuf,
player: gstreamer_player::Player,
playlist: RefCell<Vec<PlaylistItem>>,
current_item: Cell<Option<usize>>,
current_track: Cell<Option<usize>>,
playing: Cell<bool>,
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>,
track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize)>>>,
duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>,
position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
}
impl Player {
pub fn new(music_library_path: PathBuf) -> Rc<Self> {
let dispatcher = gstreamer_player::PlayerGMainContextSignalDispatcher::new(None);
let player = gstreamer_player::Player::new(None, Some(&dispatcher.upcast()));
let mut config = player.get_config();
config.set_position_update_interval(250);
player.set_config(config).unwrap();
player.set_video_track_enabled(false);
let result = Rc::new(Self {
music_library_path,
player: player.clone(),
playlist: RefCell::new(Vec::new()),
current_item: Cell::new(None),
current_track: Cell::new(None),
playing: Cell::new(false),
playlist_cbs: RefCell::new(Vec::new()),
track_cbs: RefCell::new(Vec::new()),
duration_cbs: RefCell::new(Vec::new()),
playing_cbs: RefCell::new(Vec::new()),
position_cbs: RefCell::new(Vec::new()),
});
let clone = fragile::Fragile::new(result.clone());
player.connect_end_of_stream(move |_| {
let clone = clone.get();
if clone.has_next() {
clone.next().unwrap();
} else {
clone.player.stop();
clone.playing.replace(false);
for cb in &*clone.playing_cbs.borrow() {
cb(false);
}
}
});
let clone = fragile::Fragile::new(result.clone());
player.connect_position_updated(move |_, position| {
for cb in &*clone.get().position_cbs.borrow() {
cb(position.mseconds().unwrap());
}
});
let clone = fragile::Fragile::new(result.clone());
player.connect_duration_changed(move |_, duration| {
for cb in &*clone.get().duration_cbs.borrow() {
cb(duration.mseconds().unwrap());
}
});
result
}
pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) + 'static>(&self, cb: F) {
self.playlist_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_track_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) {
self.track_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_duration_cb<F: Fn(u64) + 'static>(&self, cb: F) {
self.duration_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_playing_cb<F: Fn(bool) + 'static>(&self, cb: F) {
self.playing_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_position_cb<F: Fn(u64) + 'static>(&self, cb: F) {
self.position_cbs.borrow_mut().push(Box::new(cb));
}
pub fn get_playlist(&self) -> Vec<PlaylistItem> {
self.playlist.borrow().clone()
}
pub fn get_current_item(&self) -> Option<usize> {
self.current_item.get()
}
pub fn get_current_track(&self) -> Option<usize> {
self.current_track.get()
}
pub fn get_duration(&self) -> gstreamer::ClockTime {
self.player.get_duration()
}
pub fn is_playing(&self) -> bool {
self.playing.get()
}
pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
if item.indices.is_empty() {
Err(anyhow!(
"Tried to add playlist item without tracks to playlist!"
))
} else {
let was_empty = {
let mut playlist = self.playlist.borrow_mut();
let was_empty = playlist.is_empty();
playlist.push(item);
was_empty
};
for cb in &*self.playlist_cbs.borrow() {
cb(self.playlist.borrow().clone());
}
if was_empty {
self.set_track(0, 0)?;
self.player.play();
self.playing.set(true);
for cb in &*self.playing_cbs.borrow() {
cb(true);
}
}
Ok(())
}
}
pub fn play_pause(&self) {
if self.is_playing() {
self.player.pause();
self.playing.set(false);
for cb in &*self.playing_cbs.borrow() {
cb(false);
}
} else {
self.player.play();
self.playing.set(true);
for cb in &*self.playing_cbs.borrow() {
cb(true);
}
}
}
pub fn seek(&self, ms: u64) {
self.player.seek(gstreamer::ClockTime::from_mseconds(ms));
}
pub fn has_previous(&self) -> bool {
if let Some(current_item) = self.current_item.get() {
if let Some(current_track) = self.current_track.get() {
current_track > 0 || current_item > 0
} else {
false
}
} else {
false
}
}
pub fn previous(&self) -> Result<()> {
let mut current_item = self.current_item.get().ok_or(anyhow!("No current item!"))?;
let mut current_track = self
.current_track
.get()
.ok_or(anyhow!("No current track!"))?;
let playlist = self.playlist.borrow();
if current_track > 0 {
current_track -= 1;
} else if current_item > 0 {
current_item -= 1;
current_track = playlist[current_item].indices.len() - 1;
} else {
return Err(anyhow!("No previous track!"));
}
self.set_track(current_item, current_track)
}
pub fn has_next(&self) -> bool {
if let Some(current_item) = self.current_item.get() {
if let Some(current_track) = self.current_track.get() {
let playlist = self.playlist.borrow();
let item = &playlist[current_item];
current_track + 1 < item.indices.len() || current_item + 1 < playlist.len()
} else {
false
}
} else {
false
}
}
pub fn next(&self) -> Result<()> {
let mut current_item = self.current_item.get().ok_or(anyhow!("No current item!"))?;
let mut current_track = self
.current_track
.get()
.ok_or(anyhow!("No current track!"))?;
let playlist = self.playlist.borrow();
let item = &playlist[current_item];
if current_track + 1 < item.indices.len() {
current_track += 1;
} else if current_item + 1 < playlist.len() {
current_item += 1;
current_track = 0;
} else {
return Err(anyhow!("No next track!"));
}
self.set_track(current_item, current_track)
}
pub fn set_track(&self, current_item: usize, current_track: usize) -> Result<()> {
let uri = format!(
"file://{}",
self.music_library_path
.join(
self.playlist.borrow()[current_item].track_set.tracks[current_track].path.clone(),
)
.to_str()
.unwrap(),
);
self.player.set_uri(&uri);
if self.is_playing() {
self.player.play();
}
self.current_item.set(Some(current_item));
self.current_track.set(Some(current_track));
for cb in &*self.track_cbs.borrow() {
cb(current_item, current_track);
}
Ok(())
}
pub fn clear(&self) {
self.player.stop();
self.playing.set(false);
self.current_item.set(None);
self.current_track.set(None);
self.playlist.replace(Vec::new());
for cb in &*self.playing_cbs.borrow() {
cb(false);
}
for cb in &*self.playlist_cbs.borrow() {
cb(Vec::new());
}
}
}

View file

@ -1,9 +0,0 @@
use anyhow::Result;
pub fn init() -> Result<()> {
let bytes = glib::Bytes::from(include_bytes!(@RESOURCEFILE@).as_ref());
let resource = gio::Resource::from_data(&bytes)?;
gio::resources_register(&resource);
Ok(())
}

View file

@ -1,155 +0,0 @@
use super::*;
use crate::backend::*;
use crate::database::*;
use crate::editors::EnsembleEditor;
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 libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
pub struct EnsembleScreen {
backend: Rc<Backend>,
ensemble: Ensemble,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<Recording>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl EnsembleScreen {
pub fn new(backend: Rc<Backend>, ensemble: Ensemble) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
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);
header.set_title(Some(&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(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &Recording| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
work_label.set_halign(gtk::Align::Start);
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
performers_label.set_ellipsize(pango::EllipsizeMode::End);
performers_label.set_opacity(0.5);
performers_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&work_label);
vbox.add(&performers_label);
vbox.upcast()
});
recording_list.set_filter(
clone!(@strong search_entry => move |recording: &Recording| {
let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search)
}),
);
recording_frame.add(&recording_list.widget.clone());
let result = Rc::new(Self {
backend,
ensemble,
widget,
stack,
recording_list,
navigator: RefCell::new(None),
});
search_entry.connect_search_changed(clone!(@strong result => move |_| {
result.recording_list.invalidate_filter();
}));
back_button.connect_clicked(clone!(@strong result => move |_| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
result
.recording_list
.set_selected(clone!(@strong result => move |recording| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.push(RecordingScreen::new(result.backend.clone(), recording.clone()));
}
}));
edit_action.connect_activate(clone!(@strong result => move |_, _| {
let editor = EnsembleEditor::new(result.backend.clone(), Some(result.ensemble.clone()));
let window = NavigatorWindow::new(editor);
window.show();
}));
delete_action.connect_activate(clone!(@strong result => move |_, _| {
let context = glib::MainContext::default();
let clone = result.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 clone = result.clone();
context.spawn_local(async move {
let recordings = clone
.backend
.db()
.get_recordings_for_ensemble(&clone.ensemble.id)
.await
.unwrap();
if recordings.is_empty() {
clone.stack.set_visible_child_name("nothing");
} else {
clone.recording_list.show_items(recordings);
clone.stack.set_visible_child_name("content");
}
});
result
}
}
impl NavigatorScreen for EnsembleScreen {
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);
}
}

Some files were not shown because too many files have changed in this diff Show more