mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Move crates to toplevel directory
This commit is contained in:
parent
d16961efa8
commit
0ffe68e04f
127 changed files with 15 additions and 13 deletions
|
|
@ -1,40 +0,0 @@
|
|||
[package]
|
||||
name = "musicus"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.33"
|
||||
async-trait = "0.1.42"
|
||||
discid = "0.4.4"
|
||||
futures-channel = "0.3.5"
|
||||
gettext-rs = "0.5.0"
|
||||
gstreamer = "0.16.4"
|
||||
gtk-macros = "0.2.0"
|
||||
musicus_backend = { version = "0.1.0", path = "../musicus_backend" }
|
||||
once_cell = "1.5.2"
|
||||
rand = "0.7.3"
|
||||
|
||||
[dependencies.gdk]
|
||||
git = "https://github.com/gtk-rs/gtk4-rs/"
|
||||
package = "gdk4"
|
||||
|
||||
[dependencies.gio]
|
||||
git = "https://github.com/gtk-rs/gtk-rs/"
|
||||
features = ["v2_64"]
|
||||
|
||||
[dependencies.glib]
|
||||
git = "https://github.com/gtk-rs/gtk-rs/"
|
||||
features = ["v2_64"]
|
||||
|
||||
[dependencies.gtk]
|
||||
git = "https://github.com/gtk-rs/gtk4-rs"
|
||||
package = "gtk4"
|
||||
|
||||
[dependencies.libadwaita]
|
||||
git = "https://gitlab.gnome.org/bilelmoussaoui/libadwaita-rs"
|
||||
package = "libadwaita"
|
||||
|
||||
[dependencies.pango]
|
||||
git = "https://github.com/gtk-rs/gtk-rs/"
|
||||
features = ["v1_44"]
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Name=Musicus
|
||||
Icon=de.johrpan.musicus
|
||||
Exec=musicus
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=GTK;
|
||||
StartupNotify=true
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
de
|
||||
|
|
@ -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
|
||||
|
|
@ -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 …"
|
||||
|
|
@ -1 +0,0 @@
|
|||
i18n.gettext('musicus', preset: 'glib')
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/de/johrpan/musicus">
|
||||
<file preprocess="xml-stripblanks">ui/editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/main_screen.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/player_bar.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/player_screen.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/preferences.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/recording_editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/register_dialog.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/screen.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/section.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/selector.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/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/work_editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/work_part_editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/work_section_editor.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle" id="window_title"/>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="label" translatable="yes">Save</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<child>
|
||||
<object class="GtkBox" id="content_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-bottom">36</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title" translatable="true">Loading</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="width-request">32</property>
|
||||
<property name="height-request">32</property>
|
||||
<property name="spinning">true</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">error</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title" translatable="yes">Error</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="status_page">
|
||||
<property name="icon-name">network-error-symbolic</property>
|
||||
<property name="title">Error</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="try_again_button">
|
||||
<property name="label" translatable="yes">Try again</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="login_button">
|
||||
<property name="label" translatable="yes">Login</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="maximum-size">800</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Login to existing account</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Username</property>
|
||||
<property name="activatable-widget">username_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="username_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Password</property>
|
||||
<property name="activatable-widget">password_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="password_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="input-purpose">password</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="register_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Create a new account</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Register a new account</property>
|
||||
<property name="activatable-widget">register_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="register_button">
|
||||
<property name="label" translatable="yes">Start</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="logout_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<property name="visible">false</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Logout</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Don't use an account</property>
|
||||
<property name="activatable-widget">logout_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="logout_button">
|
||||
<property name="label" translatable="yes">Logout</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Login</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="spinning">true</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0" />
|
||||
<requires lib="libadwaita" version="1.0" />
|
||||
<object class="GtkBox" id="empty_screen">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel"/>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="icon-name">folder-music-symbolic</property>
|
||||
<property name="title" translatable="yes">Welcome to Musicus!</property>
|
||||
<property name="description" 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="vexpand">true</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwLeaflet" id="leaflet">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwLeafletPage">
|
||||
<property name="name">sidebar</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="hexpand">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Musicus</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_button">
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="receives-default">True</property>
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">menu</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchBar">
|
||||
<property name="search-mode-enabled">True</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">400</property>
|
||||
<property name="tightening-threshold">300</property>
|
||||
<property name="hexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="search_entry">
|
||||
<property name="placeholder-text" translatable="yes">Search persons and ensembles …</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkSpinner">
|
||||
<property name="spinning">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow" id="scroll">
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwLeafletPage">
|
||||
<property name="navigatable">False</property>
|
||||
<property name="child">
|
||||
<object class="GtkSeparator">
|
||||
<property name="orientation">vertical</property>
|
||||
<style>
|
||||
<class name="sidebar" />
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<menu id="menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Preferences</attribute>
|
||||
<attribute name="action">widget.preferences</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About Musicus</attribute>
|
||||
<attribute name="action">widget.about</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Import music</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="done_button">
|
||||
<property name="sensitive">False</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="done_stack">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="spinning">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="done">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">True</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<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="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">
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="name_row">
|
||||
<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="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<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="valign">center</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<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="has-frame">false</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="frame"/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkSpinner">
|
||||
<property name="spinning">true</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">error</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title" translatable="yes">Error</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="status_page">
|
||||
<property name="icon-name">dialog-error-symbolic</property>
|
||||
<property name="title">Error</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="try_again_button">
|
||||
<property name="label" translatable="yes">Try again</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">disc_error</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title" translatable="yes">Error</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="disc_status_page">
|
||||
<property name="icon-name">action-unavailable-symbolic</property>
|
||||
<property name="title">Error</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Performance</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="maximum-size">500</property>
|
||||
<property name="tightening-threshold">300</property>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="person_row">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Select a person</property>
|
||||
<property name="activatable-widget">person_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="person_button">
|
||||
<property name="label" translatable="yes">Select</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="ensemble_row">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Select an ensemble</property>
|
||||
<property name="activatable-widget">ensemble_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="ensemble_button">
|
||||
<property name="label" translatable="yes">Select</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="role_row">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Select a role</property>
|
||||
<property name="activatable-widget">role_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="reset_role_button">
|
||||
<property name="visible">false</property>
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="role_button">
|
||||
<property name="label" translatable="yes">Select</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="GtkImage" id="play_image">
|
||||
<property name="icon-name">media-playback-start-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkRevealer" id="widget">
|
||||
<property name="transition-type">slide-up</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSeparator"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="margin-top">6</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<property name="margin-start">6</property>
|
||||
<property name="margin-end">6</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="valign">center</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="previous_button">
|
||||
<property name="sensitive">False</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">media-skip-backward-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="play_button">
|
||||
<child>
|
||||
<object class="GtkImage" id="pause_image">
|
||||
<property name="icon-name">media-playback-pause-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="next_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">media-skip-forward-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="title_label">
|
||||
<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>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="subtitle_label">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Subtitle</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="position_label">
|
||||
<property name="label" translatable="yes">0:00</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">/</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="duration_label">
|
||||
<property name="label" translatable="yes">0:00</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="playlist_button">
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-list-bullet-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkImage" id="play_image">
|
||||
<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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Player</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="maximum-size">800</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="valign">center</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="previous_button">
|
||||
<property name="sensitive">False</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">media-skip-backward-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="play_button">
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="pause_image">
|
||||
<property name="icon-name">media-playback-pause-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="next_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">media-skip-forward-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="title_label">
|
||||
<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>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="subtitle_label">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Subtitle</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="stop_button">
|
||||
<property name="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">media-playback-stop-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="position_label">
|
||||
<property name="label" translatable="yes">0:00</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="position_scale">
|
||||
<property name="adjustment">position</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="duration_label">
|
||||
<property name="label" translatable="yes">0:00</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="frame">
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="AdwPreferencesWindow" id="window">
|
||||
<property name="modal">True</property>
|
||||
<property name="default-width">400</property>
|
||||
<property name="default-height">400</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesPage">
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Music library</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="music_library_path_row">
|
||||
<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="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Server connection</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="url_row">
|
||||
<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="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="login_row">
|
||||
<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="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>
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Recording</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<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="halign">start</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<property name="label" translatable="yes">Overview</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="work_row">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Select a work</property>
|
||||
<property name="activatable-widget">work_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="work_button">
|
||||
<property name="label" translatable="yes">Select</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Comment</property>
|
||||
<property name="activatable-widget">comment_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="comment_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Publish to the server</property>
|
||||
<property name="activatable-widget">upload_switch</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="upload_switch">
|
||||
<property name="valign">center</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Performers</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_performer_button">
|
||||
<property name="has-frame">false</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="performance_frame"/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Recording</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="spinning">true</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Register</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="spinning">true</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="register_button">
|
||||
<property name="label" translatable="yes">Create account</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="maximum-size">800</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Personal data</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Username</property>
|
||||
<property name="activatable-widget">username_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="username_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">E-mail (optional)</property>
|
||||
<property name="activatable-widget">email_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="email_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Password</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Password</property>
|
||||
<property name="activatable-widget">password_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="password_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="input-purpose">password</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Repeat password</property>
|
||||
<property name="activatable-widget">repeat_password_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="repeat_password_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="input-purpose">password</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Captcha</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="captcha_row">
|
||||
<property name="title-lines">0</property>
|
||||
<property name="subtitle" translatable="yes">Feel free to look for the answer online!</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Your answer</property>
|
||||
<property name="activatable-widget">captcha_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="captcha_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="activates-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle" id="window_title"/>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="menu-model">menu</property>
|
||||
<property name="icon-name">view-more-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkToggleButton" id="search_button">
|
||||
<property name="icon-name">edit-find-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchBar">
|
||||
<property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional|sync-create">False</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="hexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="search_entry"/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkSpinner">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="width-request">32</property>
|
||||
<property name="height-request">32</property>
|
||||
<property name="spinning">true</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<child>
|
||||
<object class="GtkBox" id="content_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-bottom">36</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<menu id="menu"/>
|
||||
</interface>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="title_box">
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="hexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="title_label">
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="subtitle_label">
|
||||
<property name="wrap">true</property>
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="visible">false</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="frame"/>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="width-request">250</property>
|
||||
<property name="hexpand">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar" id="header">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="title_label">
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="subtitle_label">
|
||||
<property name="visible">false</property>
|
||||
<style>
|
||||
<class name="subtitle"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="add_button">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchBar">
|
||||
<property name="search-mode-enabled">True</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">500</property>
|
||||
<property name="tightening-threshold">300</property>
|
||||
<property name="hexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="search_entry">
|
||||
<property name="placeholder-text" translatable="yes">Search …</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="server_check_button">
|
||||
<property name="label" translatable="yes">Use the Musicus server</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<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="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkSpinner">
|
||||
<property name="margin-top">12</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="spinning">True</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="height-request">200</property>
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">500</property>
|
||||
<property name="tightening-threshold">300</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="frame">
|
||||
<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>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">error</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin-start">18</property>
|
||||
<property name="margin-end">18</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">18</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">18</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="opacity">0.5</property>
|
||||
<property name="pixel-size">80</property>
|
||||
<property name="icon-name">network-error-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="opacity">0.5</property>
|
||||
<property name="label" translatable="yes">An error occured!</property>
|
||||
<attributes>
|
||||
<attribute name="size" value="16384"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<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>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="try_again_button">
|
||||
<property name="label" translatable="yes">Try again</property>
|
||||
<property name="halign">center</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="AdwWindow" id="window">
|
||||
<property name="modal">True</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Server</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="set_button">
|
||||
<property name="label" translatable="yes">Set</property>
|
||||
<property name="has-default">True</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">URL</property>
|
||||
<property name="activatable-widget">url_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle" id="window_title"/>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="vexpand">true</property>
|
||||
<property name="icon-name">folder-music-symbolic</property>
|
||||
<property name="title" translatable="yes">Import music</property>
|
||||
<property name="description" translatable="yes">Select the source which contains the new audio files below.</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="homogeneous">true</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="folder_button">
|
||||
<property name="label" translatable="true">Select folder</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disc_button">
|
||||
<property name="label" translatable="true">Copy audio CD</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title" translatable="true">Loading</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="width-request">32</property>
|
||||
<property name="height-request">32</property>
|
||||
<property name="spinning">true</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">error</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title" translatable="yes">Error</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="status_page">
|
||||
<property name="icon-name">dialog-error-symbolic</property>
|
||||
<property name="title">Error</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="try_again_button">
|
||||
<property name="label" translatable="yes">Try again</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Track</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="select_button">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">True</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<child>
|
||||
<object class="GtkFrame" id="parts_frame">
|
||||
<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>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Select tracks</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="select_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">True</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<child>
|
||||
<object class="GtkFrame" id="tracks_frame">
|
||||
<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>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Import music</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<property name="sensitive">False</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">True</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<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="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">
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="recording_row">
|
||||
<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="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<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="has-frame">false</property>
|
||||
<property name="icon-name">document-edit-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="tracks_frame"/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Work</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<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="halign">start</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<property name="label" translatable="yes">Overview</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="composer_row">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Select a composer</property>
|
||||
<property name="activatable-widget">composer_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="composer_button">
|
||||
<property name="label" translatable="yes">Select</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="activatable-widget">title_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="title_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Publish to the server</property>
|
||||
<property name="activatable-widget">upload_switch</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="upload_switch">
|
||||
<property name="valign">center</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Instruments</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_instrument_button">
|
||||
<property name="has-frame">false</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="instrument_frame"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Structure</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_section_button">
|
||||
<property name="has-frame">false</property>
|
||||
<property name="icon-name">folder-new-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_part_button">
|
||||
<property name="has-frame">false</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="structure_frame"/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Work</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="spinning">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Work part</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="maximum-size">500</property>
|
||||
<property name="tightening-threshold">300</property>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="activatable-widget">title_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="title_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkBox" id="widget">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Work section</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="maximum-size">500</property>
|
||||
<property name="tightening-threshold">300</property>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="activatable">True</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="activatable-widget">title_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="title_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
pub static VERSION: &str = "0.1.0";
|
||||
pub static LOCALEDIR: &str = "/app/share/locale";
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
pub static VERSION: &str = @VERSION@;
|
||||
pub static LOCALEDIR: &str = @LOCALEDIR@;
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use musicus_backend::db::{generate_id, Ensemble};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for creating or editing a ensemble.
|
||||
pub struct EnsembleEditor {
|
||||
handle: NavigationHandle<Ensemble>,
|
||||
|
||||
/// The ID of the ensemble that is edited or a newly generated one.
|
||||
id: String,
|
||||
|
||||
editor: Editor,
|
||||
name: EntryRow,
|
||||
upload: UploadSection,
|
||||
}
|
||||
|
||||
impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
|
||||
/// Create a new ensemble editor and optionally initialize it.
|
||||
fn new(ensemble: Option<Ensemble>, handle: NavigationHandle<Ensemble>) -> Rc<Self> {
|
||||
let editor = Editor::new();
|
||||
editor.set_title("Ensemble/Role");
|
||||
|
||||
let list = gtk::ListBoxBuilder::new()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.build();
|
||||
|
||||
let name = EntryRow::new(&gettext("Name"));
|
||||
list.append(&name.widget);
|
||||
|
||||
let section = Section::new(&gettext("General"), &list);
|
||||
let upload = UploadSection::new();
|
||||
|
||||
editor.add_content(§ion.widget);
|
||||
editor.add_content(&upload.widget);
|
||||
|
||||
let id = match ensemble {
|
||||
Some(ensemble) => {
|
||||
name.set_text(&ensemble.name);
|
||||
ensemble.id
|
||||
}
|
||||
None => generate_id(),
|
||||
};
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
id,
|
||||
editor,
|
||||
name,
|
||||
upload,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.editor.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.editor.set_save_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
this.editor.loading();
|
||||
match this.save().await {
|
||||
Ok(ensemble) => {
|
||||
this.handle.pop(Some(ensemble));
|
||||
}
|
||||
Err(err) => {
|
||||
let description = gettext!("Cause: {}", err);
|
||||
this.editor.error(&gettext("Failed to save ensemble!"), &description);
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl EnsembleEditor {
|
||||
/// Save the ensemble and possibly upload it to the server.
|
||||
async fn save(&self) -> Result<Ensemble> {
|
||||
let name = self.name.get_text();
|
||||
|
||||
let ensemble = Ensemble {
|
||||
id: self.id.clone(),
|
||||
name,
|
||||
};
|
||||
|
||||
if self.upload.get_active() {
|
||||
self.handle.backend.cl().post_ensemble(&ensemble).await?;
|
||||
}
|
||||
|
||||
self.handle.backend.db().update_ensemble(ensemble.clone()).await?;
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(ensemble)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for EnsembleEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.editor.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use musicus_backend::db::{generate_id, Instrument};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for creating or editing a instrument.
|
||||
pub struct InstrumentEditor {
|
||||
handle: NavigationHandle<Instrument>,
|
||||
|
||||
/// The ID of the instrument that is edited or a newly generated one.
|
||||
id: String,
|
||||
|
||||
editor: Editor,
|
||||
name: EntryRow,
|
||||
upload: UploadSection,
|
||||
}
|
||||
|
||||
impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
|
||||
/// Create a new instrument editor and optionally initialize it.
|
||||
fn new(instrument: Option<Instrument>, handle: NavigationHandle<Instrument>) -> Rc<Self> {
|
||||
let editor = Editor::new();
|
||||
editor.set_title("Instrument/Role");
|
||||
|
||||
let list = gtk::ListBoxBuilder::new()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.build();
|
||||
|
||||
let name = EntryRow::new(&gettext("Name"));
|
||||
list.append(&name.widget);
|
||||
|
||||
let section = Section::new(&gettext("General"), &list);
|
||||
let upload = UploadSection::new();
|
||||
|
||||
editor.add_content(§ion.widget);
|
||||
editor.add_content(&upload.widget);
|
||||
|
||||
let id = match instrument {
|
||||
Some(instrument) => {
|
||||
name.set_text(&instrument.name);
|
||||
instrument.id
|
||||
}
|
||||
None => generate_id(),
|
||||
};
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
id,
|
||||
editor,
|
||||
name,
|
||||
upload,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.editor.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.editor.set_save_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
this.editor.loading();
|
||||
match this.save().await {
|
||||
Ok(instrument) => {
|
||||
this.handle.pop(Some(instrument));
|
||||
}
|
||||
Err(err) => {
|
||||
let description = gettext!("Cause: {}", err);
|
||||
this.editor.error(&gettext("Failed to save instrument!"), &description);
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl InstrumentEditor {
|
||||
/// Save the instrument and possibly upload it to the server.
|
||||
async fn save(&self) -> Result<Instrument> {
|
||||
let name = self.name.get_text();
|
||||
|
||||
let instrument = Instrument {
|
||||
id: self.id.clone(),
|
||||
name,
|
||||
};
|
||||
|
||||
if self.upload.get_active() {
|
||||
self.handle.backend.cl().post_instrument(&instrument).await?;
|
||||
}
|
||||
|
||||
self.handle.backend.db().update_instrument(instrument.clone()).await?;
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(instrument)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for InstrumentEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.editor.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::selectors::{EnsembleSelector, InstrumentSelector, PersonSelector};
|
||||
use crate::widgets::{Editor, Section, ButtonRow, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{Performance, Person, Ensemble, Instrument};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for editing a performance within a recording.
|
||||
pub struct PerformanceEditor {
|
||||
handle: NavigationHandle<Performance>,
|
||||
editor: Editor,
|
||||
person_row: ButtonRow,
|
||||
ensemble_row: ButtonRow,
|
||||
role_row: ButtonRow,
|
||||
reset_role_button: gtk::Button,
|
||||
person: RefCell<Option<Person>>,
|
||||
ensemble: RefCell<Option<Ensemble>>,
|
||||
role: RefCell<Option<Instrument>>,
|
||||
}
|
||||
|
||||
impl Screen<Option<Performance>, Performance> for PerformanceEditor {
|
||||
/// Create a new performance editor.
|
||||
fn new(performance: Option<Performance>, handle: NavigationHandle<Performance>) -> Rc<Self> {
|
||||
let editor = Editor::new();
|
||||
editor.set_title("Performance");
|
||||
editor.set_may_save(false);
|
||||
|
||||
let performer_list = gtk::ListBoxBuilder::new()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.build();
|
||||
|
||||
let person_row = ButtonRow::new("Person", "Select");
|
||||
let ensemble_row = ButtonRow::new("Ensemble", "Select");
|
||||
|
||||
performer_list.append(&person_row.get_widget());
|
||||
performer_list.append(&ensemble_row.get_widget());
|
||||
|
||||
let performer_section = Section::new(&gettext("Performer"), &performer_list);
|
||||
performer_section.set_subtitle(
|
||||
&gettext("Select either a person or an ensemble as a performer."));
|
||||
|
||||
let role_list = gtk::ListBoxBuilder::new()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.build();
|
||||
|
||||
let reset_role_button = gtk::ButtonBuilder::new()
|
||||
.icon_name("user-trash-symbolic")
|
||||
.valign(gtk::Align::Center)
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
let role_row = ButtonRow::new("Role", "Select");
|
||||
role_row.widget.add_suffix(&reset_role_button);
|
||||
|
||||
role_list.append(&role_row.get_widget());
|
||||
|
||||
let role_section = Section::new(&gettext("Role"), &role_list);
|
||||
role_section.set_subtitle(
|
||||
&gettext("Optionally, choose a role to specify what the performer does."));
|
||||
|
||||
editor.add_content(&performer_section);
|
||||
editor.add_content(&role_section);
|
||||
|
||||
let this = Rc::new(PerformanceEditor {
|
||||
handle,
|
||||
editor,
|
||||
person_row,
|
||||
ensemble_row,
|
||||
role_row,
|
||||
reset_role_button,
|
||||
person: RefCell::new(None),
|
||||
ensemble: RefCell::new(None),
|
||||
role: RefCell::new(None),
|
||||
});
|
||||
|
||||
this.editor.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.editor.set_save_cb(clone!(@weak this => move || {
|
||||
let performance = Performance {
|
||||
person: this.person.borrow().clone(),
|
||||
ensemble: this.ensemble.borrow().clone(),
|
||||
role: this.role.borrow().clone(),
|
||||
};
|
||||
|
||||
this.handle.pop(Some(performance));
|
||||
}));
|
||||
|
||||
this.person_row.set_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(person) = push!(this.handle, PersonSelector).await {
|
||||
this.show_person(Some(&person));
|
||||
this.person.replace(Some(person.clone()));
|
||||
this.show_ensemble(None);
|
||||
this.ensemble.replace(None);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.ensemble_row.set_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(ensemble) = push!(this.handle, EnsembleSelector).await {
|
||||
this.show_person(None);
|
||||
this.person.replace(None);
|
||||
this.show_ensemble(Some(&ensemble));
|
||||
this.ensemble.replace(Some(ensemble));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.role_row.set_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(role) = push!(this.handle, InstrumentSelector).await {
|
||||
this.show_role(Some(&role));
|
||||
this.role.replace(Some(role));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.reset_role_button.connect_clicked(clone!(@weak 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
|
||||
}
|
||||
}
|
||||
|
||||
impl PerformanceEditor {
|
||||
/// Update the UI according to person.
|
||||
fn show_person(&self, person: Option<&Person>) {
|
||||
if let Some(person) = person {
|
||||
self.person_row.set_subtitle(Some(&person.name_fl()));
|
||||
self.editor.set_may_save(true);
|
||||
} else {
|
||||
self.person_row.set_subtitle(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the UI according to ensemble.
|
||||
fn show_ensemble(&self, ensemble: Option<&Ensemble>) {
|
||||
if let Some(ensemble) = ensemble {
|
||||
self.ensemble_row.set_subtitle(Some(&ensemble.name));
|
||||
self.editor.set_may_save(true);
|
||||
} else {
|
||||
self.ensemble_row.set_subtitle(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the UI according to role.
|
||||
fn show_role(&self, role: Option<&Instrument>) {
|
||||
if let Some(role) = role {
|
||||
self.role_row.set_subtitle(Some(&role.name));
|
||||
self.reset_role_button.show();
|
||||
} else {
|
||||
self.role_row.set_subtitle(None);
|
||||
self.reset_role_button.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PerformanceEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.editor.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use musicus_backend::db::{generate_id, Person};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for creating or editing a person.
|
||||
pub struct PersonEditor {
|
||||
handle: NavigationHandle<Person>,
|
||||
|
||||
/// The ID of the person that is edited or a newly generated one.
|
||||
id: String,
|
||||
|
||||
editor: Editor,
|
||||
first_name: EntryRow,
|
||||
last_name: EntryRow,
|
||||
upload: UploadSection,
|
||||
}
|
||||
|
||||
impl Screen<Option<Person>, Person> for PersonEditor {
|
||||
/// Create a new person editor and optionally initialize it.
|
||||
fn new(person: Option<Person>, handle: NavigationHandle<Person>) -> Rc<Self> {
|
||||
let editor = Editor::new();
|
||||
editor.set_title("Person");
|
||||
|
||||
let list = gtk::ListBoxBuilder::new()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.build();
|
||||
|
||||
let first_name = EntryRow::new(&gettext("First name"));
|
||||
let last_name = EntryRow::new(&gettext("Last name"));
|
||||
|
||||
list.append(&first_name.widget);
|
||||
list.append(&last_name.widget);
|
||||
|
||||
let section = Section::new(&gettext("General"), &list);
|
||||
let upload = UploadSection::new();
|
||||
|
||||
editor.add_content(§ion.widget);
|
||||
editor.add_content(&upload.widget);
|
||||
|
||||
let id = match person {
|
||||
Some(person) => {
|
||||
first_name.set_text(&person.first_name);
|
||||
last_name.set_text(&person.last_name);
|
||||
|
||||
person.id
|
||||
}
|
||||
None => generate_id(),
|
||||
};
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
id,
|
||||
editor,
|
||||
first_name,
|
||||
last_name,
|
||||
upload,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.editor.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.editor.set_save_cb(clone!(@strong this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
this.editor.loading();
|
||||
match this.save().await {
|
||||
Ok(person) => {
|
||||
this.handle.pop(Some(person));
|
||||
}
|
||||
Err(err) => {
|
||||
let description = gettext!("Cause: {}", err);
|
||||
this.editor.error(&gettext("Failed to save person!"), &description);
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl PersonEditor {
|
||||
/// Save the person and possibly upload it to the server.
|
||||
async fn save(self: &Rc<Self>) -> Result<Person> {
|
||||
let first_name = self.first_name.get_text();
|
||||
let last_name = self.last_name.get_text();
|
||||
|
||||
let person = Person {
|
||||
id: self.id.clone(),
|
||||
first_name,
|
||||
last_name,
|
||||
};
|
||||
|
||||
if self.upload.get_active() {
|
||||
self.handle.backend.cl().post_person(&person).await?;
|
||||
}
|
||||
|
||||
self.handle.backend.db().update_person(person.clone()).await?;
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(person)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PersonEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.editor.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
use super::performance::PerformanceEditor;
|
||||
use crate::selectors::WorkSelector;
|
||||
use crate::widgets::{List, Widget};
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{generate_id, Performance, Recording, Work};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A widget for creating or editing a recording.
|
||||
pub struct RecordingEditor {
|
||||
handle: NavigationHandle<Recording>,
|
||||
widget: gtk::Stack,
|
||||
save_button: gtk::Button,
|
||||
info_bar: gtk::InfoBar,
|
||||
work_row: libadwaita::ActionRow,
|
||||
comment_entry: gtk::Entry,
|
||||
upload_switch: gtk::Switch,
|
||||
performance_list: Rc<List>,
|
||||
id: String,
|
||||
work: RefCell<Option<Work>>,
|
||||
performances: RefCell<Vec<Performance>>,
|
||||
}
|
||||
|
||||
impl Screen<Option<Recording>, Recording> for RecordingEditor {
|
||||
/// Create a new recording editor widget and optionally initialize it.
|
||||
fn new(recording: Option<Recording>, handle: NavigationHandle<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, libadwaita::ActionRow, work_row);
|
||||
get_widget!(builder, gtk::Button, work_button);
|
||||
get_widget!(builder, gtk::Entry, comment_entry);
|
||||
get_widget!(builder, gtk::Switch, upload_switch);
|
||||
get_widget!(builder, gtk::Frame, performance_frame);
|
||||
get_widget!(builder, gtk::Button, add_performer_button);
|
||||
|
||||
let performance_list = List::new();
|
||||
performance_frame.set_child(Some(&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 {
|
||||
handle,
|
||||
widget,
|
||||
save_button,
|
||||
info_bar,
|
||||
work_row,
|
||||
comment_entry,
|
||||
upload_switch,
|
||||
performance_list,
|
||||
id,
|
||||
work: RefCell::new(work),
|
||||
performances: RefCell::new(performances),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.save_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
match this.save().await {
|
||||
Ok(recording) => {
|
||||
this.handle.pop(Some(recording));
|
||||
}
|
||||
Err(_) => {
|
||||
this.info_bar.set_revealed(true);
|
||||
this.widget.set_visible_child_name("content");
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
work_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(work) = push!(this.handle, WorkSelector).await {
|
||||
this.work_selected(&work);
|
||||
this.work.replace(Some(work));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.performance_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let performance = &this.performances.borrow()[index];
|
||||
|
||||
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
|
||||
delete_button.set_valign(gtk::Align::Center);
|
||||
|
||||
delete_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let length = {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances.remove(index);
|
||||
performances.len()
|
||||
};
|
||||
|
||||
this.performance_list.update(length);
|
||||
}));
|
||||
|
||||
let edit_button = gtk::Button::from_icon_name(Some("document-edit-symbolic"));
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
|
||||
edit_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
let performance = &this.performances.borrow()[index];
|
||||
if let Some(performance) = push!(this.handle, PerformanceEditor, Some(performance.to_owned())).await {
|
||||
let length = {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances[index] = performance;
|
||||
performances.len()
|
||||
};
|
||||
|
||||
this.performance_list.update(length);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&performance.get_title()));
|
||||
row.add_suffix(&delete_button);
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
add_performer_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(performance) = push!(this.handle, PerformanceEditor, None).await {
|
||||
let length = {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances.push(performance);
|
||||
performances.len()
|
||||
};
|
||||
|
||||
this.performance_list.update(length);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// Initialize
|
||||
|
||||
if let Some(work) = &*this.work.borrow() {
|
||||
this.work_selected(work);
|
||||
}
|
||||
|
||||
let length = this.performances.borrow().len();
|
||||
this.performance_list.update(length);
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl RecordingEditor {
|
||||
/// Update the UI according to work.
|
||||
fn work_selected(&self, work: &Work) {
|
||||
self.work_row.set_title(Some(&gettext("Work")));
|
||||
self.work_row.set_subtitle(Some(&work.get_title()));
|
||||
self.save_button.set_sensitive(true);
|
||||
}
|
||||
|
||||
/// Save the recording and possibly upload it to the server.
|
||||
async fn save(self: &Rc<Self>) -> Result<Recording> {
|
||||
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().unwrap().to_string(),
|
||||
performances: self.performances.borrow().clone(),
|
||||
};
|
||||
|
||||
let upload = self.upload_switch.get_active();
|
||||
if upload {
|
||||
self.handle.backend.cl().post_recording(&recording).await?;
|
||||
}
|
||||
|
||||
self.handle.backend
|
||||
.db()
|
||||
.update_recording(recording.clone().into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(recording)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RecordingEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,357 +0,0 @@
|
|||
use super::work_part::WorkPartEditor;
|
||||
use super::work_section::WorkSectionEditor;
|
||||
use crate::selectors::{InstrumentSelector, PersonSelector};
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{List, Widget};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{generate_id, Instrument, Person, Work, WorkPart, WorkSection};
|
||||
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),
|
||||
}
|
||||
|
||||
impl PartOrSection {
|
||||
pub fn get_title(&self) -> String {
|
||||
match self {
|
||||
PartOrSection::Part(part) => part.title.clone(),
|
||||
PartOrSection::Section(section) => section.title.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget for editing and creating works.
|
||||
pub struct WorkEditor {
|
||||
handle: NavigationHandle<Work>,
|
||||
widget: gtk::Stack,
|
||||
save_button: gtk::Button,
|
||||
title_entry: gtk::Entry,
|
||||
info_bar: gtk::InfoBar,
|
||||
composer_row: libadwaita::ActionRow,
|
||||
upload_switch: gtk::Switch,
|
||||
instrument_list: Rc<List>,
|
||||
part_list: Rc<List>,
|
||||
id: String,
|
||||
composer: RefCell<Option<Person>>,
|
||||
instruments: RefCell<Vec<Instrument>>,
|
||||
structure: RefCell<Vec<PartOrSection>>,
|
||||
}
|
||||
|
||||
impl Screen<Option<Work>, Work> for WorkEditor {
|
||||
/// Create a new work editor widget and optionally initialize it.
|
||||
fn new(work: Option<Work>, handle: NavigationHandle<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, libadwaita::ActionRow, composer_row);
|
||||
get_widget!(builder, gtk::Switch, upload_switch);
|
||||
get_widget!(builder, gtk::Frame, instrument_frame);
|
||||
get_widget!(builder, gtk::Button, add_instrument_button);
|
||||
get_widget!(builder, gtk::Frame, structure_frame);
|
||||
get_widget!(builder, gtk::Button, add_part_button);
|
||||
get_widget!(builder, gtk::Button, add_section_button);
|
||||
|
||||
let instrument_list = List::new();
|
||||
instrument_frame.set_child(Some(&instrument_list.widget));
|
||||
|
||||
let part_list = List::new();
|
||||
part_list.set_enable_dnd(true);
|
||||
structure_frame.set_child(Some(&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 {
|
||||
handle,
|
||||
widget,
|
||||
save_button,
|
||||
id,
|
||||
info_bar,
|
||||
title_entry,
|
||||
composer_row,
|
||||
upload_switch,
|
||||
instrument_list,
|
||||
part_list,
|
||||
composer: RefCell::new(composer),
|
||||
instruments: RefCell::new(instruments),
|
||||
structure: RefCell::new(structure),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.save_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
match this.save().await {
|
||||
Ok(work) => {
|
||||
this.handle.pop(Some(work));
|
||||
}
|
||||
Err(_) => {
|
||||
this.info_bar.set_revealed(true);
|
||||
this.widget.set_visible_child_name("content");
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
composer_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(person) = push!(this.handle, PersonSelector).await {
|
||||
this.show_composer(&person);
|
||||
this.composer.replace(Some(person.to_owned()));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.instrument_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let instrument = &this.instruments.borrow()[index];
|
||||
|
||||
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
|
||||
delete_button.set_valign(gtk::Align::Center);
|
||||
|
||||
delete_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let length = {
|
||||
let mut instruments = this.instruments.borrow_mut();
|
||||
instruments.remove(index);
|
||||
instruments.len()
|
||||
};
|
||||
|
||||
this.instrument_list.update(length);
|
||||
}));
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_title(Some(&instrument.name));
|
||||
row.add_suffix(&delete_button);
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
add_instrument_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(instrument) = push!(this.handle, InstrumentSelector).await {
|
||||
let length = {
|
||||
let mut instruments = this.instruments.borrow_mut();
|
||||
instruments.push(instrument.clone());
|
||||
instruments.len()
|
||||
};
|
||||
|
||||
this.instrument_list.update(length);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.part_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let pos = &this.structure.borrow()[index];
|
||||
|
||||
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
|
||||
delete_button.set_valign(gtk::Align::Center);
|
||||
|
||||
delete_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.remove(index);
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
}));
|
||||
|
||||
let edit_button = gtk::Button::from_icon_name(Some("document-edit-symbolic"));
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
|
||||
edit_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
match this.structure.borrow()[index].clone() {
|
||||
PartOrSection::Part(part) => {
|
||||
if let Some(part) = push!(this.handle, WorkPartEditor, Some(part)).await {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure[index] = PartOrSection::Part(part);
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
}
|
||||
}
|
||||
PartOrSection::Section(section) => {
|
||||
if let Some(section) = push!(this.handle, WorkSectionEditor, Some(section)).await {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure[index] = PartOrSection::Section(section);
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&pos.get_title()));
|
||||
row.add_suffix(&delete_button);
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
|
||||
if let PartOrSection::Part(_) = pos {
|
||||
// TODO: Replace with better solution to differentiate parts and sections.
|
||||
row.set_margin_start(12);
|
||||
}
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.part_list.set_move_cb(clone!(@weak this => move |old_index, new_index| {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.swap(old_index, new_index);
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
}));
|
||||
|
||||
add_part_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(part) = push!(this.handle, WorkPartEditor, None).await {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.push(PartOrSection::Part(part));
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
add_section_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(section) = push!(this.handle, WorkSectionEditor, None).await {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.push(PartOrSection::Section(section));
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// Initialization
|
||||
|
||||
if let Some(composer) = &*this.composer.borrow() {
|
||||
this.show_composer(composer);
|
||||
}
|
||||
|
||||
this.instrument_list.update(this.instruments.borrow().len());
|
||||
this.part_list.update(this.structure.borrow().len());
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkEditor {
|
||||
/// Update the UI according to person.
|
||||
fn show_composer(&self, person: &Person) {
|
||||
self.composer_row.set_title(Some(&gettext("Composer")));
|
||||
self.composer_row.set_subtitle(Some(&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<Work> {
|
||||
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().unwrap().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.handle.backend.cl().post_work(&work).await?;
|
||||
}
|
||||
|
||||
self.handle.backend
|
||||
.db()
|
||||
.update_work(work.clone().into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(work)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WorkEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::db::WorkPart;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for creating or editing a work section.
|
||||
pub struct WorkPartEditor {
|
||||
handle: NavigationHandle<WorkPart>,
|
||||
widget: gtk::Box,
|
||||
title_entry: gtk::Entry,
|
||||
}
|
||||
|
||||
impl Screen<Option<WorkPart>, WorkPart> for WorkPartEditor {
|
||||
/// Create a new part editor and optionally initialize it.
|
||||
fn new(section: Option<WorkPart>, handle: NavigationHandle<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);
|
||||
|
||||
if let Some(section) = section {
|
||||
title_entry.set_text(§ion.title);
|
||||
}
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
title_entry,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
save_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let section = WorkPart {
|
||||
title: this.title_entry.get_text().unwrap().to_string(),
|
||||
};
|
||||
|
||||
this.handle.pop(Some(section));
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WorkPartEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::db::WorkSection;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for creating or editing a work section.
|
||||
pub struct WorkSectionEditor {
|
||||
handle: NavigationHandle<WorkSection>,
|
||||
widget: gtk::Box,
|
||||
title_entry: gtk::Entry,
|
||||
}
|
||||
|
||||
impl Screen<Option<WorkSection>, WorkSection> for WorkSectionEditor {
|
||||
/// Create a new section editor and optionally initialize it.
|
||||
fn new(section: Option<WorkSection>, handle: NavigationHandle<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(§ion.title);
|
||||
}
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
title_entry,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
save_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let section = WorkSection {
|
||||
before_index: 0,
|
||||
title: this.title_entry.get_text().unwrap().to_string(),
|
||||
};
|
||||
|
||||
this.handle.pop(Some(section));
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WorkSectionEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
use super::source::{Source, SourceTrack};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use discid::DiscId;
|
||||
use futures_channel::oneshot;
|
||||
use gettextrs::gettext;
|
||||
use gstreamer::prelude::*;
|
||||
use gstreamer::{Element, ElementFactory, Pipeline};
|
||||
use once_cell::sync::OnceCell;
|
||||
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: OnceCell<String>,
|
||||
|
||||
/// The tracks on this disc.
|
||||
tracks: OnceCell<Vec<SourceTrack>>,
|
||||
}
|
||||
|
||||
impl DiscSource {
|
||||
/// Create a new disc source. The source has to be initialized by calling
|
||||
/// load() afterwards.
|
||||
pub fn new() -> Result<Self> {
|
||||
let result = Self {
|
||||
discid: OnceCell::new(),
|
||||
tracks: OnceCell::new(),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Load the disc from the default disc drive and return the MusicBrainz
|
||||
/// DiscID as well as descriptions of the contained tracks.
|
||||
fn load_priv() -> Result<(String, Vec<SourceTrack>)> {
|
||||
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 name = gettext!("Track {}", number);
|
||||
|
||||
let file_name = format!("track_{:02}.flac", number);
|
||||
|
||||
let mut path = tmp_dir.clone();
|
||||
path.push(file_name);
|
||||
|
||||
let track = SourceTrack {
|
||||
number,
|
||||
name,
|
||||
path,
|
||||
};
|
||||
|
||||
tracks.push(track);
|
||||
}
|
||||
|
||||
Ok((id, tracks))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Source for DiscSource {
|
||||
async fn load(&self) -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
thread::spawn(|| {
|
||||
let result = Self::load_priv();
|
||||
sender.send(result).unwrap();
|
||||
});
|
||||
|
||||
let (discid, tracks) = receiver.await??;
|
||||
|
||||
self.discid.set(discid).unwrap();
|
||||
self.tracks.set(tracks).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tracks(&self) -> Option<&[SourceTrack]> {
|
||||
match self.tracks.get() {
|
||||
Some(tracks) => Some(tracks.as_slice()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn discid(&self) -> Option<String> {
|
||||
match self.discid.get() {
|
||||
Some(discid) => Some(discid.to_owned()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn copy(&self) -> Result<()> {
|
||||
let tracks = self.tracks.get()
|
||||
.ok_or_else(|| anyhow!("Tried to copy disc before loading has finished!"))?;
|
||||
|
||||
for track in 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
use super::source::{Source, SourceTrack};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures_channel::oneshot;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
|
||||
/// A folder outside of the music library that contains tracks to import.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FolderSource {
|
||||
/// The path to the folder.
|
||||
path: PathBuf,
|
||||
|
||||
/// The tracks within the folder.
|
||||
tracks: OnceCell<Vec<SourceTrack>>,
|
||||
}
|
||||
|
||||
impl FolderSource {
|
||||
/// Create a new folder source.
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
tracks: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the contents of the folder as tracks.
|
||||
fn load_priv(path: &Path) -> Result<Vec<SourceTrack>> {
|
||||
let mut tracks = Vec::new();
|
||||
let mut number = 1;
|
||||
|
||||
for entry in std::fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
|
||||
if entry.file_type()?.is_file() {
|
||||
let name = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.or_else(|_| Err(anyhow!("Failed to convert OsString to String!")))?;
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
let track = SourceTrack {
|
||||
number,
|
||||
name,
|
||||
path,
|
||||
};
|
||||
|
||||
tracks.push(track);
|
||||
number += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tracks)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Source for FolderSource {
|
||||
async fn load(&self) -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
let path = self.path.clone();
|
||||
thread::spawn(move || {
|
||||
let result = Self::load_priv(&path);
|
||||
sender.send(result).unwrap();
|
||||
});
|
||||
|
||||
let tracks = receiver.await??;
|
||||
self.tracks.set(tracks).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tracks(&self) -> Option<&[SourceTrack]> {
|
||||
match self.tracks.get() {
|
||||
Some(tracks) => Some(tracks.as_slice()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn discid(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn copy(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
use super::source::Source;
|
||||
use super::track_set_editor::{TrackSetData, TrackSetEditor};
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{List, Widget};
|
||||
use anyhow::{anyhow, Result};
|
||||
use glib::clone;
|
||||
use glib::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{generate_id, Medium, Track, TrackSet};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for editing metadata while importing music into the music library.
|
||||
pub struct MediumEditor {
|
||||
handle: NavigationHandle<()>,
|
||||
source: Rc<Box<dyn Source>>,
|
||||
widget: gtk::Stack,
|
||||
done_button: gtk::Button,
|
||||
done_stack: gtk::Stack,
|
||||
done: gtk::Image,
|
||||
name_entry: gtk::Entry,
|
||||
publish_switch: gtk::Switch,
|
||||
status_page: libadwaita::StatusPage,
|
||||
disc_status_page: libadwaita::StatusPage,
|
||||
track_set_list: Rc<List>,
|
||||
track_sets: RefCell<Vec<TrackSetData>>,
|
||||
}
|
||||
|
||||
impl Screen<Rc<Box<dyn Source>>, ()> for MediumEditor {
|
||||
/// Create a new medium editor.
|
||||
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<()>) -> 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);
|
||||
get_widget!(builder, libadwaita::StatusPage, status_page);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
get_widget!(builder, libadwaita::StatusPage, disc_status_page);
|
||||
get_widget!(builder, gtk::Button, cancel_button);
|
||||
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
source,
|
||||
widget,
|
||||
done_button,
|
||||
done_stack,
|
||||
done,
|
||||
name_entry,
|
||||
publish_switch,
|
||||
status_page,
|
||||
disc_status_page,
|
||||
track_set_list: list,
|
||||
track_sets: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.done_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
spawn!(@clone this, async move {
|
||||
match this.save().await {
|
||||
Ok(_) => this.handle.pop(Some(())),
|
||||
Err(err) => {
|
||||
this.status_page.set_description(Some(&err.to_string()));
|
||||
this.widget.set_visible_child_name("error");
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
add_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(track_set) = push!(this.handle, TrackSetEditor, Rc::clone(&this.source)).await {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.track_set_list.set_make_widget_cb(clone!(@weak 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"));
|
||||
let edit_button = gtk::Button::new();
|
||||
edit_button.set_has_frame(false);
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
edit_button.set_child(Some(&edit_image));
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&title));
|
||||
row.set_subtitle(Some(&subtitle));
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
|
||||
edit_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
// TODO: Implement editing.
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
try_again_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.widget.set_visible_child_name("content");
|
||||
}));
|
||||
|
||||
cancel_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
match this.source.copy().await {
|
||||
Err(err) => {
|
||||
this.disc_status_page.set_description(Some(&err.to_string()));
|
||||
this.widget.set_visible_child_name("disc_error");
|
||||
},
|
||||
Ok(_) => {
|
||||
this.done_stack.set_visible_child(&this.done);
|
||||
this.done_button.set_sensitive(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl MediumEditor {
|
||||
/// Save the medium and possibly upload it to the server.
|
||||
async fn save(&self) -> Result<()> {
|
||||
let name = self.name_entry.get_text().unwrap().to_string();
|
||||
|
||||
// Create a new directory in the music library path for the imported medium.
|
||||
|
||||
let mut path = self.handle.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();
|
||||
let source_tracks = self.source.tracks().ok_or_else(|| anyhow!("Tracks not loaded!"))?;
|
||||
|
||||
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 = &source_tracks[track_data.track_source];
|
||||
|
||||
let mut track_path = path.clone();
|
||||
track_path.push(track_source.path.file_name().unwrap());
|
||||
|
||||
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().unwrap().to_string(),
|
||||
discid: self.source.discid(),
|
||||
tracks: track_sets,
|
||||
};
|
||||
|
||||
let upload = self.publish_switch.get_active();
|
||||
if upload {
|
||||
self.handle.backend.cl().post_medium(&medium).await?;
|
||||
}
|
||||
|
||||
self.handle.backend
|
||||
.db()
|
||||
.update_medium(medium.clone())
|
||||
.await?;
|
||||
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MediumEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
mod disc_source;
|
||||
mod folder_source;
|
||||
mod medium_editor;
|
||||
mod source;
|
||||
mod source_selector;
|
||||
mod track_editor;
|
||||
mod track_selector;
|
||||
mod track_set_editor;
|
||||
|
||||
pub use source_selector::SourceSelector;
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A source for tracks that can be imported into the music library.
|
||||
#[async_trait]
|
||||
pub trait Source {
|
||||
/// Load the source and discover the contained tracks.
|
||||
async fn load(&self) -> Result<()>;
|
||||
|
||||
/// Get a reference to the tracks within this source, if they are ready.
|
||||
fn tracks(&self) -> Option<&[SourceTrack]>;
|
||||
|
||||
/// Get the DiscID of the corresponging medium, if possible.
|
||||
fn discid(&self) -> Option<String>;
|
||||
|
||||
/// Asynchronously copy the tracks to the files that are advertised within
|
||||
/// their corresponding objects.
|
||||
async fn copy(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Representation of a single track on a source.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SourceTrack {
|
||||
/// 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,
|
||||
|
||||
/// A human readable identifier for the track. This will be used to
|
||||
/// present the track for selection.
|
||||
pub name: String,
|
||||
|
||||
/// The path to the file where the corresponding audio file is. This file
|
||||
/// is only required to exist, once the source's copy method has finished.
|
||||
/// This will not be the actual file within the user's music library, but
|
||||
/// the location from which it can be copied to the music library.
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
use super::medium_editor::MediumEditor;
|
||||
use super::disc_source::DiscSource;
|
||||
use super::folder_source::FolderSource;
|
||||
use super::source::Source;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for starting to import music.
|
||||
pub struct SourceSelector {
|
||||
handle: NavigationHandle<()>,
|
||||
widget: gtk::Stack,
|
||||
status_page: libadwaita::StatusPage,
|
||||
}
|
||||
|
||||
impl Screen<(), ()> for SourceSelector {
|
||||
/// Create a new source selector.
|
||||
fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui");
|
||||
|
||||
get_widget!(builder, gtk::Stack, widget);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Button, folder_button);
|
||||
get_widget!(builder, gtk::Button, disc_button);
|
||||
get_widget!(builder, libadwaita::StatusPage, status_page);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
status_page,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
folder_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let dialog = gtk::FileChooserDialog::new(
|
||||
Some(&gettext("Select folder")),
|
||||
Some(&this.handle.window),
|
||||
gtk::FileChooserAction::SelectFolder,
|
||||
&[
|
||||
(&gettext("Cancel"), gtk::ResponseType::Cancel),
|
||||
(&gettext("Select"), gtk::ResponseType::Accept),
|
||||
]);
|
||||
|
||||
dialog.set_modal(true);
|
||||
|
||||
dialog.connect_response(clone!(@weak this => move |dialog, response| {
|
||||
dialog.hide();
|
||||
|
||||
if let gtk::ResponseType::Accept = response {
|
||||
if let Some(file) = dialog.get_file() {
|
||||
if let Some(path) = file.get_path() {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let folder = FolderSource::new(PathBuf::from(path));
|
||||
match folder.load().await {
|
||||
Ok(_) => {
|
||||
let source = Rc::new(Box::new(folder) as Box<dyn Source>);
|
||||
push!(this.handle, MediumEditor, source).await;
|
||||
this.handle.pop(Some(()));
|
||||
}
|
||||
Err(err) => {
|
||||
this.status_page.set_description(Some(&err.to_string()));
|
||||
this.widget.set_visible_child_name("error");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
dialog.show();
|
||||
}));
|
||||
|
||||
disc_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let disc = DiscSource::new().unwrap();
|
||||
match disc.load().await {
|
||||
Ok(_) => {
|
||||
let source = Rc::new(Box::new(disc) as Box<dyn Source>);
|
||||
push!(this.handle, MediumEditor, source).await;
|
||||
this.handle.pop(Some(()));
|
||||
}
|
||||
Err(err) => {
|
||||
this.status_page.set_description(Some(&err.to_string()));
|
||||
this.widget.set_visible_child_name("error");
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
try_again_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.widget.set_visible_child_name("content");
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SourceSelector {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::Recording;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for editing a single track.
|
||||
pub struct TrackEditor {
|
||||
handle: NavigationHandle<Vec<usize>>,
|
||||
widget: gtk::Box,
|
||||
selection: RefCell<Vec<usize>>,
|
||||
}
|
||||
|
||||
impl Screen<(Recording, Vec<usize>), Vec<usize>> for TrackEditor {
|
||||
/// Create a new track editor.
|
||||
fn new((recording, selection): (Recording, Vec<usize>), handle: NavigationHandle<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.set_child(Some(&parts_list));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
selection: RefCell::new(selection),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
select_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let selection = this.selection.borrow().clone();
|
||||
this.handle.pop(Some(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!(@weak 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 = libadwaita::ActionRow::new();
|
||||
row.add_prefix(&check);
|
||||
row.set_activatable_widget(Some(&check));
|
||||
row.set_title(Some(&part.title));
|
||||
|
||||
parts_list.append(&row);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TrackEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
use super::source::Source;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for selecting tracks from a source.
|
||||
pub struct TrackSelector {
|
||||
handle: NavigationHandle<Vec<usize>>,
|
||||
source: Rc<Box<dyn Source>>,
|
||||
widget: gtk::Box,
|
||||
select_button: gtk::Button,
|
||||
selection: RefCell<Vec<usize>>,
|
||||
}
|
||||
|
||||
impl Screen<Rc<Box<dyn Source>>, Vec<usize>> for TrackSelector {
|
||||
/// Create a new track selector.
|
||||
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<Vec<usize>>) -> 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.set_child(Some(&track_list));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
source,
|
||||
widget,
|
||||
select_button,
|
||||
selection: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.select_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let selection = this.selection.borrow().clone();
|
||||
this.handle.pop(Some(selection));
|
||||
}));
|
||||
|
||||
let tracks = this.source.tracks().unwrap();
|
||||
|
||||
for (index, track) in tracks.iter().enumerate() {
|
||||
let check = gtk::CheckButton::new();
|
||||
|
||||
check.connect_toggled(clone!(@weak 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 row = libadwaita::ActionRow::new();
|
||||
row.add_prefix(&check);
|
||||
row.set_activatable_widget(Some(&check));
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&track.name));
|
||||
|
||||
track_list.append(&row);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TrackSelector {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
use super::source::Source;
|
||||
use super::track_editor::TrackEditor;
|
||||
use super::track_selector::TrackSelector;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::selectors::RecordingSelector;
|
||||
use crate::widgets::{List, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::Recording;
|
||||
use std::cell::RefCell;
|
||||
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 {
|
||||
handle: NavigationHandle<TrackSetData>,
|
||||
source: Rc<Box<dyn Source>>,
|
||||
widget: gtk::Box,
|
||||
save_button: gtk::Button,
|
||||
recording_row: libadwaita::ActionRow,
|
||||
track_list: Rc<List>,
|
||||
recording: RefCell<Option<Recording>>,
|
||||
tracks: RefCell<Vec<TrackData>>,
|
||||
}
|
||||
|
||||
impl Screen<Rc<Box<dyn Source>>, TrackSetData> for TrackSetEditor {
|
||||
/// Create a new track set editor.
|
||||
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<TrackSetData>) -> 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, libadwaita::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();
|
||||
tracks_frame.set_child(Some(&track_list.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
source,
|
||||
widget,
|
||||
save_button,
|
||||
recording_row,
|
||||
track_list,
|
||||
recording: RefCell::new(None),
|
||||
tracks: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.save_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let data = TrackSetData {
|
||||
recording: this.recording.borrow().clone().unwrap(),
|
||||
tracks: this.tracks.borrow().clone(),
|
||||
};
|
||||
|
||||
this.handle.pop(Some(data));
|
||||
}));
|
||||
|
||||
select_recording_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(recording) = push!(this.handle, RecordingSelector).await {
|
||||
this.recording.replace(Some(recording));
|
||||
this.recording_selected();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
edit_tracks_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(selection) = push!(this.handle, TrackSelector, Rc::clone(&this.source)).await {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.track_list.set_make_widget_cb(clone!(@weak 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 tracks = this.source.tracks().unwrap();
|
||||
let track_name = &tracks[track.track_source].name;
|
||||
|
||||
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
|
||||
let edit_button = gtk::Button::new();
|
||||
edit_button.set_has_frame(false);
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
edit_button.set_child(Some(&edit_image));
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&title));
|
||||
row.set_subtitle(Some(track_name));
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
|
||||
edit_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let recording = this.recording.borrow().clone();
|
||||
if let Some(recording) = recording {
|
||||
spawn!(@clone this, async move {
|
||||
let track = &this.tracks.borrow()[index];
|
||||
if let Some(selection) = push!(this.handle, TrackEditor, (recording, track.work_parts.clone())).await {
|
||||
{
|
||||
let mut tracks = this.tracks.borrow_mut();
|
||||
let mut track = &mut tracks[index];
|
||||
track.work_parts = selection;
|
||||
};
|
||||
|
||||
this.update_tracks();
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl TrackSetEditor {
|
||||
/// 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 Widget for TrackSetEditor {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/// Simplification for pushing new screens.
|
||||
///
|
||||
/// This macro can be invoked in two forms.
|
||||
///
|
||||
/// 1. To push screens without an input value:
|
||||
///
|
||||
/// ```
|
||||
/// let result = push!(handle, ScreenType).await;
|
||||
/// ```
|
||||
///
|
||||
/// 2. To push screens with an input value:
|
||||
///
|
||||
/// ```
|
||||
/// let result = push!(handle, ScreenType, input).await;
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! push {
|
||||
($handle:expr, $screen:ty) => {
|
||||
$handle.push::<_, _, $screen>(())
|
||||
};
|
||||
($handle:expr, $screen:ty, $input:expr) => {
|
||||
$handle.push::<_, _, $screen>($input)
|
||||
};
|
||||
}
|
||||
|
||||
/// Simplification for replacing the current navigator screen.
|
||||
///
|
||||
/// This macro can be invoked in two forms.
|
||||
///
|
||||
/// 1. To replace with screens without an input value:
|
||||
///
|
||||
/// ```
|
||||
/// let result = replace!(navigator, ScreenType).await;
|
||||
/// ```
|
||||
///
|
||||
/// 2. To replace with screens with an input value:
|
||||
///
|
||||
/// ```
|
||||
/// let result = replace!(navigator, ScreenType, input).await;
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! replace {
|
||||
($navigator:expr, $screen:ty) => {
|
||||
$navigator.replace::<_, _, $screen>(())
|
||||
};
|
||||
($navigator:expr, $screen:ty, $input:expr) => {
|
||||
$navigator.replace::<_, _, $screen>($input)
|
||||
};
|
||||
}
|
||||
|
||||
/// Spawn a future on the GLib MainContext.
|
||||
///
|
||||
/// This can be invoked in the following forms:
|
||||
///
|
||||
/// 1. For spawning a future and nothing more:
|
||||
///
|
||||
/// ```
|
||||
/// spawn!(async {
|
||||
/// // Some code
|
||||
/// });
|
||||
///
|
||||
/// 2. For spawning a future and cloning some data, that will be accessible
|
||||
/// from the async code:
|
||||
///
|
||||
/// ```
|
||||
/// spawn!(@clone data: Rc<_>, async move {
|
||||
/// // Some code
|
||||
/// });
|
||||
#[macro_export]
|
||||
macro_rules! spawn {
|
||||
($future:expr) => {
|
||||
{
|
||||
let context = glib::MainContext::default();
|
||||
context.spawn_local($future);
|
||||
|
||||
}
|
||||
};
|
||||
(@clone $data:ident, $future:expr) => {
|
||||
{
|
||||
let context = glib::MainContext::default();
|
||||
let $data = Rc::clone(&$data);
|
||||
context.spawn_local($future);
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use gio::prelude::*;
|
||||
use glib::clone;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
mod config;
|
||||
mod editors;
|
||||
mod import;
|
||||
mod navigator;
|
||||
mod preferences;
|
||||
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!");
|
||||
libadwaita::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);
|
||||
}
|
||||
|
|
@ -1,59 +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(
|
||||
'config.rs',
|
||||
'resources.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,
|
||||
build_always_stale: true,
|
||||
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(),
|
||||
]
|
||||
)
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
use crate::widgets::Widget;
|
||||
use futures_channel::oneshot;
|
||||
use futures_channel::oneshot::{Receiver, Sender};
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use musicus_backend::Backend;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::{Rc, Weak};
|
||||
|
||||
pub mod window;
|
||||
pub use window::*;
|
||||
|
||||
/// A widget that represents a logical unit of transient user interaction and
|
||||
/// that optionally resolves to a specific return value.
|
||||
pub trait Screen<I, O>: Widget {
|
||||
/// Create a new screen and initialize it with the provided input value.
|
||||
fn new(input: I, navigation_handle: NavigationHandle<O>) -> Rc<Self> where Self: Sized;
|
||||
}
|
||||
|
||||
/// An accessor to navigation functionality for screens.
|
||||
pub struct NavigationHandle<O> {
|
||||
/// The backend, in case the screen needs it.
|
||||
pub backend: Rc<Backend>,
|
||||
|
||||
/// The toplevel window, in case the screen needs it.
|
||||
pub window: gtk::Window,
|
||||
|
||||
/// The navigator that created this navigation handle.
|
||||
navigator: Weak<Navigator>,
|
||||
|
||||
/// The sender through which the result should be sent.
|
||||
sender: Cell<Option<Sender<Option<O>>>>,
|
||||
}
|
||||
|
||||
impl<O> NavigationHandle<O> {
|
||||
/// Switch to another screen and wait for that screen's result.
|
||||
pub async fn push<I, R, S: Screen<I, R> + 'static>(&self, input: I) -> Option<R> {
|
||||
let navigator = self.unwrap_navigator();
|
||||
let receiver = navigator.push::<I, R, S>(input);
|
||||
|
||||
// If the sender is dropped, return None.
|
||||
receiver.await.unwrap_or(None)
|
||||
}
|
||||
|
||||
/// Go back to the previous screen optionally returning something.
|
||||
pub fn pop(&self, output: Option<O>) {
|
||||
self.unwrap_navigator().pop();
|
||||
|
||||
let sender = self.sender.take()
|
||||
.expect("Tried to send result from screen through a dropped sender.");
|
||||
|
||||
if sender.send(output).is_err() {
|
||||
panic!("Tried to send result from screen to non-existing previous screen.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the navigator and panic if it doesn't exist.
|
||||
fn unwrap_navigator(&self) -> Rc<Navigator> {
|
||||
Weak::upgrade(&self.navigator)
|
||||
.expect("Tried to access non-existing navigator from a screen.")
|
||||
}
|
||||
}
|
||||
|
||||
/// A toplevel widget for managing screens.
|
||||
pub struct Navigator {
|
||||
/// The underlying GTK widget.
|
||||
pub widget: gtk::Stack,
|
||||
|
||||
/// The backend, in case screens need it.
|
||||
backend: Rc<Backend>,
|
||||
|
||||
/// The toplevel window of the navigator, in case screens need it.
|
||||
window: gtk::Window,
|
||||
|
||||
/// The currently active screens. The last screen in this vector is the one
|
||||
/// that is currently visible.
|
||||
screens: RefCell<Vec<Rc<dyn Widget>>>,
|
||||
|
||||
/// A vector holding the widgets of the old screens that are waiting to be
|
||||
/// removed after the animation has finished.
|
||||
old_widgets: RefCell<Vec<gtk::Widget>>,
|
||||
|
||||
/// A closure that will be called when the last screen is popped.
|
||||
back_cb: RefCell<Option<Box<dyn Fn()>>>,
|
||||
}
|
||||
|
||||
impl Navigator {
|
||||
/// Create a new navigator which will display the provided widget
|
||||
/// initially.
|
||||
pub fn new<W, E>(backend: Rc<Backend>, window: &W, empty_screen: &E) -> Rc<Self>
|
||||
where
|
||||
W: IsA<gtk::Window>,
|
||||
E: IsA<gtk::Widget>,
|
||||
{
|
||||
let widget = gtk::StackBuilder::new()
|
||||
.hhomogeneous(false)
|
||||
.vhomogeneous(false)
|
||||
.interpolate_size(true)
|
||||
.transition_type(gtk::StackTransitionType::Crossfade)
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
|
||||
widget.add_named(empty_screen, Some("empty_screen"));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
backend,
|
||||
window: window.to_owned().upcast(),
|
||||
screens: RefCell::new(Vec::new()),
|
||||
old_widgets: RefCell::new(Vec::new()),
|
||||
back_cb: RefCell::new(None),
|
||||
});
|
||||
|
||||
this.widget.connect_property_transition_running_notify(clone!(@strong this => move |_| {
|
||||
if !this.widget.get_transition_running() {
|
||||
this.clear_old_widgets();
|
||||
}
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// Set the closure to be called when the last screen is popped so that
|
||||
/// the navigator shows its empty state.
|
||||
pub fn set_back_cb<F: Fn() + 'static>(&self, cb: F) {
|
||||
self.back_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Drop all screens and show the provided screen instead.
|
||||
pub async fn replace<I, O, S: Screen<I, O> + 'static>(self: &Rc<Self>, input: I) -> Option<O> {
|
||||
for screen in self.screens.replace(Vec::new()) {
|
||||
self.old_widgets.borrow_mut().push(screen.get_widget());
|
||||
}
|
||||
|
||||
let receiver = self.push::<I, O, S>(input);
|
||||
|
||||
if !self.widget.get_transition_running() {
|
||||
self.clear_old_widgets();
|
||||
}
|
||||
|
||||
// We ignore the case, if a sender is dropped.
|
||||
receiver.await.unwrap_or(None)
|
||||
}
|
||||
|
||||
|
||||
/// Drop all screens and go back to the initial screen. The back callback
|
||||
/// will not be called.
|
||||
pub fn reset(&self) {
|
||||
self.widget.set_visible_child_name("empty_screen");
|
||||
|
||||
for screen in self.screens.replace(Vec::new()) {
|
||||
self.old_widgets.borrow_mut().push(screen.get_widget());
|
||||
}
|
||||
|
||||
if !self.widget.get_transition_running() {
|
||||
self.clear_old_widgets();
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a screen with the provided input. This should only be called from
|
||||
/// within a navigation handle.
|
||||
fn push<I, O, S: Screen<I, O> + 'static>(self: &Rc<Self>, input: I) -> Receiver<Option<O>> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
let handle = NavigationHandle {
|
||||
backend: Rc::clone(&self.backend),
|
||||
window: self.window.clone(),
|
||||
navigator: Rc::downgrade(self),
|
||||
sender: Cell::new(Some(sender)),
|
||||
};
|
||||
|
||||
let screen = S::new(input, handle);
|
||||
|
||||
let widget = screen.get_widget();
|
||||
self.widget.add_child(&widget);
|
||||
self.widget.set_visible_child(&widget);
|
||||
|
||||
self.screens.borrow_mut().push(screen);
|
||||
|
||||
receiver
|
||||
}
|
||||
|
||||
/// Pop the last screen from the list of screens.
|
||||
fn pop(&self) {
|
||||
let popped = if let Some(screen) = self.screens.borrow_mut().pop() {
|
||||
let widget = screen.get_widget();
|
||||
self.old_widgets.borrow_mut().push(widget);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if popped {
|
||||
if let Some(screen) = self.screens.borrow().last() {
|
||||
let widget = screen.get_widget();
|
||||
self.widget.set_visible_child(&widget);
|
||||
} else {
|
||||
self.widget.set_visible_child_name("empty_screen");
|
||||
|
||||
if let Some(cb) = &*self.back_cb.borrow() {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
if !self.widget.get_transition_running() {
|
||||
self.clear_old_widgets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the old widgets.
|
||||
fn clear_old_widgets(&self) {
|
||||
for widget in self.old_widgets.borrow().iter() {
|
||||
self.widget.remove(widget);
|
||||
}
|
||||
|
||||
self.old_widgets.borrow_mut().clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
use super::Navigator;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use musicus_backend::Backend;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A window hosting a navigator.
|
||||
pub struct NavigatorWindow {
|
||||
pub navigator: Rc<Navigator>,
|
||||
window: libadwaita::Window,
|
||||
}
|
||||
|
||||
impl NavigatorWindow {
|
||||
/// Create a new navigator window and show it.
|
||||
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
|
||||
let window = libadwaita::Window::new();
|
||||
window.set_default_size(600, 424);
|
||||
let placeholder = gtk::Label::new(None);
|
||||
let navigator = Navigator::new(backend, &window, &placeholder);
|
||||
libadwaita::WindowExt::set_child(&window, Some(&navigator.widget));
|
||||
|
||||
let this = Rc::new(Self { navigator, window });
|
||||
|
||||
this.navigator.set_back_cb(clone!(@strong this => move || {
|
||||
this.window.close();
|
||||
}));
|
||||
|
||||
this.window.show();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// Make the wrapped window transient. This will make the window modal.
|
||||
pub fn set_transient_for<W: IsA<gtk::Window>>(&self, window: &W) {
|
||||
self.window.set_modal(true);
|
||||
self.window.set_transient_for(Some(window));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
use super::register::RegisterDialog;
|
||||
use crate::push;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::client::LoginData;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for entering login credentials.
|
||||
pub struct LoginDialog {
|
||||
handle: NavigationHandle<Option<LoginData>>,
|
||||
widget: gtk::Stack,
|
||||
info_bar: gtk::InfoBar,
|
||||
username_entry: gtk::Entry,
|
||||
password_entry: gtk::Entry,
|
||||
}
|
||||
|
||||
impl Screen<Option<LoginData>, Option<LoginData>> for LoginDialog {
|
||||
fn new(data: Option<LoginData>, handle: NavigationHandle<Option<LoginData>>) -> Rc<Self> {
|
||||
// Create UI
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/login_dialog.ui");
|
||||
|
||||
get_widget!(builder, gtk::Stack, widget);
|
||||
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);
|
||||
get_widget!(builder, gtk::Box, register_box);
|
||||
get_widget!(builder, gtk::Button, register_button);
|
||||
get_widget!(builder, gtk::Box, logout_box);
|
||||
get_widget!(builder, gtk::Button, logout_button);
|
||||
|
||||
if let Some(data) = data {
|
||||
username_entry.set_text(&data.username);
|
||||
register_box.hide();
|
||||
logout_box.show();
|
||||
}
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
info_bar,
|
||||
username_entry,
|
||||
password_entry,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
cancel_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
login_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
|
||||
let data = LoginData {
|
||||
username: this.username_entry.get_text().unwrap().to_string(),
|
||||
password: this.password_entry.get_text().unwrap().to_string(),
|
||||
};
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.set_login_data(Some(data.clone())).await;
|
||||
if this.handle.backend.cl().login().await.unwrap() {
|
||||
this.handle.pop(Some(Some(data)));
|
||||
} else {
|
||||
this.widget.set_visible_child_name("content");
|
||||
this.info_bar.set_revealed(true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
register_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(data) = push!(this.handle, RegisterDialog).await {
|
||||
this.handle.pop(Some(Some(data)));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
logout_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.set_login_data(None).await;
|
||||
this.handle.pop(Some(None));
|
||||
});
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for LoginDialog {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
use crate::navigator::NavigatorWindow;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::Backend;
|
||||
use libadwaita::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
mod login;
|
||||
use login::LoginDialog;
|
||||
|
||||
mod server;
|
||||
use server::ServerDialog;
|
||||
|
||||
mod register;
|
||||
|
||||
/// A dialog for configuring the app.
|
||||
pub struct Preferences {
|
||||
backend: Rc<Backend>,
|
||||
window: libadwaita::Window,
|
||||
music_library_path_row: libadwaita::ActionRow,
|
||||
url_row: libadwaita::ActionRow,
|
||||
login_row: libadwaita::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, libadwaita::Window, window);
|
||||
get_widget!(builder, libadwaita::ActionRow, music_library_path_row);
|
||||
get_widget!(builder, gtk::Button, select_music_library_path_button);
|
||||
get_widget!(builder, libadwaita::ActionRow, url_row);
|
||||
get_widget!(builder, gtk::Button, url_button);
|
||||
get_widget!(builder, libadwaita::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::FileChooserDialog::new(
|
||||
Some(&gettext("Select music library folder")),
|
||||
Some(&this.window),
|
||||
gtk::FileChooserAction::SelectFolder,
|
||||
&[
|
||||
(&gettext("Cancel"), gtk::ResponseType::Cancel),
|
||||
(&gettext("Select"), gtk::ResponseType::Accept),
|
||||
]);
|
||||
|
||||
dialog.set_modal(true);
|
||||
|
||||
dialog.connect_response(clone!(@strong this => move |dialog, response| {
|
||||
if let gtk::ResponseType::Accept = response {
|
||||
if let Some(file) = dialog.get_file() {
|
||||
if let Some(path) = file.get_path() {
|
||||
this.music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
this.backend.set_music_library_path(path).await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
}));
|
||||
|
||||
dialog.show();
|
||||
}));
|
||||
|
||||
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 window = NavigatorWindow::new(this.backend.clone());
|
||||
window.set_transient_for(&this.window);
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(data) = replace!(window.navigator, LoginDialog, this.backend.get_login_data()).await {
|
||||
if let Some(data) = data {
|
||||
this.login_row.set_subtitle(Some(&data.username));
|
||||
} else {
|
||||
this.login_row.set_subtitle(Some(&gettext("Not logged in")));
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::client::{LoginData, UserRegistration};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for creating a new user account.
|
||||
pub struct RegisterDialog {
|
||||
handle: NavigationHandle<LoginData>,
|
||||
widget: gtk::Stack,
|
||||
username_entry: gtk::Entry,
|
||||
email_entry: gtk::Entry,
|
||||
password_entry: gtk::Entry,
|
||||
repeat_password_entry: gtk::Entry,
|
||||
captcha_row: libadwaita::ActionRow,
|
||||
captcha_entry: gtk::Entry,
|
||||
captcha_id: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl Screen<(), LoginData> for RegisterDialog {
|
||||
/// Create a new register dialog.
|
||||
fn new(_: (), handle: NavigationHandle<LoginData>) -> Rc<Self> {
|
||||
// Create UI
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/register_dialog.ui");
|
||||
|
||||
get_widget!(builder, gtk::Stack, widget);
|
||||
get_widget!(builder, gtk::Button, cancel_button);
|
||||
get_widget!(builder, gtk::Button, register_button);
|
||||
get_widget!(builder, gtk::Entry, username_entry);
|
||||
get_widget!(builder, gtk::Entry, email_entry);
|
||||
get_widget!(builder, gtk::Entry, password_entry);
|
||||
get_widget!(builder, gtk::Entry, repeat_password_entry);
|
||||
get_widget!(builder, libadwaita::ActionRow, captcha_row);
|
||||
get_widget!(builder, gtk::Entry, captcha_entry);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
username_entry,
|
||||
email_entry,
|
||||
password_entry,
|
||||
repeat_password_entry,
|
||||
captcha_row,
|
||||
captcha_entry,
|
||||
captcha_id: RefCell::new(None),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
cancel_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
register_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let password = this.password_entry.get_text().unwrap().to_string();
|
||||
let repeat = this.repeat_password_entry.get_text().unwrap().to_string();
|
||||
|
||||
if (password != repeat) {
|
||||
// TODO: Show error and validate other input.
|
||||
} else {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let username = this.username_entry.get_text().unwrap().to_string();
|
||||
let email = this.email_entry.get_text().unwrap().to_string();
|
||||
let captcha_id = this.captcha_id.borrow().clone().unwrap();
|
||||
let answer = this.captcha_entry.get_text().unwrap().to_string();
|
||||
|
||||
let email = if email.len() == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(email)
|
||||
};
|
||||
|
||||
let registration = UserRegistration {
|
||||
username: username.clone(),
|
||||
password: password.clone(),
|
||||
email,
|
||||
captcha_id,
|
||||
answer,
|
||||
};
|
||||
|
||||
// TODO: Handle errors.
|
||||
if this.handle.backend.cl().register(registration).await.unwrap() {
|
||||
let data = LoginData {
|
||||
username,
|
||||
password,
|
||||
};
|
||||
|
||||
this.handle.pop(Some(data));
|
||||
} else {
|
||||
this.widget.set_visible_child_name("content");
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Initialize
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let captcha = this.handle.backend.cl().get_captcha().await.unwrap();
|
||||
this.captcha_row.set_title(Some(&captcha.question));
|
||||
this.captcha_id.replace(Some(captcha.id));
|
||||
this.widget.set_visible_child_name("content");
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RegisterDialog {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::Backend;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for setting up the server.
|
||||
pub struct ServerDialog {
|
||||
backend: Rc<Backend>,
|
||||
window: libadwaita::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, libadwaita::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().unwrap().to_string();
|
||||
this.backend.set_server_url(&url);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
use anyhow::Result;
|
||||
|
||||
pub fn init() -> Result<()> {
|
||||
let bytes = glib::Bytes::from(include_bytes!("/home/johrpan/.var/app/org.gnome.Builder/cache/gnome-builder/projects/musicus/builds/de.johrpan.musicus.json-flatpak-org.gnome.Platform-x86_64-master-master/crates/musicus/res/musicus.gresource").as_ref());
|
||||
let resource = gio::Resource::from_data(&bytes)?;
|
||||
gio::resources_register(&resource);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
use super::{MediumScreen, RecordingScreen};
|
||||
use crate::editors::EnsembleEditor;
|
||||
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
|
||||
use crate::widgets;
|
||||
use crate::widgets::{List, Section, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{Ensemble, Medium, Recording};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for showing recordings with a ensemble.
|
||||
pub struct EnsembleScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
ensemble: Ensemble,
|
||||
widget: widgets::Screen,
|
||||
recording_list: Rc<List>,
|
||||
medium_list: Rc<List>,
|
||||
recordings: RefCell<Vec<Recording>>,
|
||||
mediums: RefCell<Vec<Medium>>,
|
||||
}
|
||||
|
||||
impl Screen<Ensemble, ()> for EnsembleScreen {
|
||||
/// Create a new ensemble screen for the specified ensemble and load the
|
||||
/// contents asynchronously.
|
||||
fn new(ensemble: Ensemble, handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let widget = widgets::Screen::new();
|
||||
widget.set_title(&ensemble.name);
|
||||
|
||||
let recording_list = List::new();
|
||||
let medium_list = List::new();
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
ensemble,
|
||||
widget,
|
||||
recording_list,
|
||||
medium_list,
|
||||
recordings: RefCell::new(Vec::new()),
|
||||
mediums: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
this.widget.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
|
||||
this.widget.add_action(&gettext("Edit ensemble"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
let window = NavigatorWindow::new(this.handle.backend.clone());
|
||||
replace!(window.navigator, EnsembleEditor, Some(this.ensemble.clone())).await;
|
||||
});
|
||||
}));
|
||||
|
||||
this.widget.add_action(&gettext("Delete ensemble"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.db().delete_ensemble(&this.ensemble.id).await.unwrap();
|
||||
this.handle.backend.library_changed();
|
||||
});
|
||||
}));
|
||||
|
||||
this.widget.set_search_cb(clone!(@weak this => move || {
|
||||
this.recording_list.invalidate_filter();
|
||||
this.medium_list.invalidate_filter();
|
||||
}));
|
||||
|
||||
this.recording_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
let recording = recording.clone();
|
||||
spawn!(@clone this, async move {
|
||||
push!(this.handle, RecordingScreen, recording.clone()).await;
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.recording_list.set_filter_cb(clone!(@weak this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
let search = this.widget.get_search();
|
||||
let text = recording.work.get_title() + &recording.get_performers();
|
||||
search.is_empty() || text.to_lowercase().contains(&search)
|
||||
}));
|
||||
|
||||
this.medium_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let medium = &this.mediums.borrow()[index];
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&medium.name));
|
||||
|
||||
let medium = medium.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
let medium = medium.clone();
|
||||
spawn!(@clone this, async move {
|
||||
push!(this.handle, MediumScreen, medium.clone()).await;
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.medium_list.set_filter_cb(clone!(@weak this => move |index| {
|
||||
let medium = &this.mediums.borrow()[index];
|
||||
let search = this.widget.get_search();
|
||||
let name = medium.name.to_lowercase();
|
||||
search.is_empty() || name.contains(&search)
|
||||
}));
|
||||
|
||||
// Load the content asynchronously.
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let recordings = this.handle
|
||||
.backend
|
||||
.db()
|
||||
.get_recordings_for_ensemble(&this.ensemble.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mediums = this.handle
|
||||
.backend
|
||||
.db()
|
||||
.get_mediums_for_ensemble(&this.ensemble.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !recordings.is_empty() {
|
||||
let length = recordings.len();
|
||||
this.recordings.replace(recordings);
|
||||
this.recording_list.update(length);
|
||||
|
||||
let section = Section::new("Recordings", &this.recording_list.widget);
|
||||
this.widget.add_content(§ion.widget);
|
||||
}
|
||||
|
||||
if !mediums.is_empty() {
|
||||
let length = mediums.len();
|
||||
this.mediums.replace(mediums);
|
||||
this.medium_list.update(length);
|
||||
|
||||
let section = Section::new("Mediums", &this.medium_list.widget);
|
||||
this.widget.add_content(§ion.widget);
|
||||
}
|
||||
|
||||
this.widget.ready();
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for EnsembleScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
use super::{EnsembleScreen, PersonScreen, PlayerScreen};
|
||||
use crate::config;
|
||||
use crate::import::SourceSelector;
|
||||
use crate::navigator::{Navigator, NavigatorWindow, NavigationHandle, Screen};
|
||||
use crate::preferences::Preferences;
|
||||
use crate::widgets::{List, PlayerBar, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{Ensemble, Person};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Either a person or an ensemble to be shown in the list.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PersonOrEnsemble {
|
||||
Person(Person),
|
||||
Ensemble(Ensemble),
|
||||
}
|
||||
|
||||
impl PersonOrEnsemble {
|
||||
/// Get a short textual representation of the item.
|
||||
pub fn get_title(&self) -> String {
|
||||
match self {
|
||||
PersonOrEnsemble::Person(person) => person.name_lf(),
|
||||
PersonOrEnsemble::Ensemble(ensemble) => ensemble.name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The main screen of the app, once it's set up and finished loading. The screen assumes that the
|
||||
/// music library and the player are available and initialized.
|
||||
pub struct MainScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
widget: gtk::Box,
|
||||
leaflet: libadwaita::Leaflet,
|
||||
search_entry: gtk::SearchEntry,
|
||||
stack: gtk::Stack,
|
||||
poe_list: Rc<List>,
|
||||
navigator: Rc<Navigator>,
|
||||
poes: RefCell<Vec<PersonOrEnsemble>>,
|
||||
}
|
||||
|
||||
impl Screen<(), ()> for MainScreen {
|
||||
/// Create a new main screen.
|
||||
fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/main_screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, libadwaita::Leaflet, leaflet);
|
||||
get_widget!(builder, gtk::Button, add_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::ScrolledWindow, scroll);
|
||||
get_widget!(builder, gtk::Box, empty_screen);
|
||||
|
||||
let actions = gio::SimpleActionGroup::new();
|
||||
let preferences_action = gio::SimpleAction::new("preferences", None);
|
||||
let about_action = gio::SimpleAction::new("about", None);
|
||||
actions.add_action(&preferences_action);
|
||||
actions.add_action(&about_action);
|
||||
widget.insert_action_group("widget", Some(&actions));
|
||||
|
||||
let poe_list = List::new();
|
||||
poe_list.widget.add_css_class("navigation-sidebar");
|
||||
poe_list.enable_selection();
|
||||
|
||||
let navigator = Navigator::new(Rc::clone(&handle.backend), &handle.window, &empty_screen);
|
||||
|
||||
scroll.set_child(Some(&poe_list.widget));
|
||||
leaflet.append(&navigator.widget);
|
||||
|
||||
let player_bar = PlayerBar::new();
|
||||
widget.append(&player_bar.widget);
|
||||
player_bar.set_player(Some(Rc::clone(&handle.backend.pl())));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
leaflet,
|
||||
search_entry,
|
||||
stack,
|
||||
poe_list,
|
||||
navigator,
|
||||
poes: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
preferences_action.connect_activate(clone!(@weak this => move |_, _| {
|
||||
Preferences::new(Rc::clone(&this.handle.backend), &this.handle.window).show();
|
||||
}));
|
||||
|
||||
about_action.connect_activate(clone!(@weak this => move |_, _| {
|
||||
this.show_about_dialog();
|
||||
}));
|
||||
|
||||
add_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
let window = NavigatorWindow::new(Rc::clone(&this.handle.backend));
|
||||
replace!(window.navigator, SourceSelector).await;
|
||||
});
|
||||
}));
|
||||
|
||||
this.search_entry.connect_search_changed(clone!(@weak this => move |_| {
|
||||
this.poe_list.invalidate_filter();
|
||||
}));
|
||||
|
||||
this.poe_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let poe = &this.poes.borrow()[index];
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&poe.get_title()));
|
||||
|
||||
let poe = poe.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
let poe = poe.clone();
|
||||
spawn!(@clone this, async move {
|
||||
this.leaflet.set_visible_child(&this.navigator.widget);
|
||||
|
||||
match poe {
|
||||
PersonOrEnsemble::Person(person) => {
|
||||
replace!(this.navigator, PersonScreen, person).await;
|
||||
}
|
||||
PersonOrEnsemble::Ensemble(ensemble) => {
|
||||
replace!(this.navigator, EnsembleScreen, ensemble).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.poe_list.set_filter_cb(clone!(@weak this => move |index| {
|
||||
let poe = &this.poes.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
let title = poe.get_title().to_lowercase();
|
||||
search.is_empty() || title.contains(&search)
|
||||
}));
|
||||
|
||||
this.navigator.set_back_cb(clone!(@weak this => move || {
|
||||
this.leaflet.set_visible_child_name("sidebar");
|
||||
}));
|
||||
|
||||
player_bar.set_playlist_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
push!(this.handle, PlayerScreen).await;
|
||||
});
|
||||
}));
|
||||
|
||||
// Load the content asynchronously.
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let mut poes = Vec::new();
|
||||
|
||||
let persons = this.handle.backend.db().get_persons().await.unwrap();
|
||||
let ensembles = this.handle.backend.db().get_ensembles().await.unwrap();
|
||||
|
||||
for person in persons {
|
||||
poes.push(PersonOrEnsemble::Person(person));
|
||||
}
|
||||
|
||||
for ensemble in ensembles {
|
||||
poes.push(PersonOrEnsemble::Ensemble(ensemble));
|
||||
}
|
||||
|
||||
let length = poes.len();
|
||||
this.poes.replace(poes);
|
||||
this.poe_list.update(length);
|
||||
|
||||
this.stack.set_visible_child_name("content");
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MainScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
impl MainScreen {
|
||||
/// Show a dialog with information on this application.
|
||||
fn show_about_dialog(&self) {
|
||||
let dialog = gtk::AboutDialogBuilder::new()
|
||||
.transient_for(&self.handle.window)
|
||||
.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.show();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets;
|
||||
use crate::widgets::{List, Section, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::PlaylistItem;
|
||||
use musicus_backend::db::Medium;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Elements for visually representing the contents of the medium.
|
||||
enum ListItem {
|
||||
/// A header shown on top of a track set. The value is the index of the corresponding track set
|
||||
/// within the medium.
|
||||
Header(usize),
|
||||
|
||||
/// A track. The indices are from the track set and the track.
|
||||
Track(usize, usize),
|
||||
|
||||
/// A separator shown between track sets.
|
||||
Separator,
|
||||
}
|
||||
|
||||
/// A screen for showing the contents of a medium.
|
||||
pub struct MediumScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
medium: Medium,
|
||||
widget: widgets::Screen,
|
||||
list: Rc<List>,
|
||||
items: Vec<ListItem>,
|
||||
}
|
||||
|
||||
impl Screen<Medium, ()> for MediumScreen {
|
||||
/// Create a new medium screen for the specified medium and load the
|
||||
/// contents asynchronously.
|
||||
fn new(medium: Medium, handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let mut items = Vec::new();
|
||||
let mut first = true;
|
||||
|
||||
for (track_set_index, track_set) in medium.tracks.iter().enumerate() {
|
||||
if !first {
|
||||
items.push(ListItem::Separator);
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
items.push(ListItem::Header(track_set_index));
|
||||
|
||||
for (track_index, _) in track_set.tracks.iter().enumerate() {
|
||||
items.push(ListItem::Track(track_set_index, track_index));
|
||||
}
|
||||
}
|
||||
|
||||
let widget = widgets::Screen::new();
|
||||
widget.set_title(&medium.name);
|
||||
|
||||
let list = List::new();
|
||||
let section = Section::new("Recordings", &list.widget);
|
||||
widget.add_content(§ion.widget);
|
||||
widget.ready();
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
medium,
|
||||
widget,
|
||||
list,
|
||||
items,
|
||||
});
|
||||
|
||||
this.widget.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
|
||||
this.widget.add_action(&gettext("Edit medium"), clone!(@weak this => move || {
|
||||
// TODO: Show medium editor.
|
||||
}));
|
||||
|
||||
this.widget.add_action(&gettext("Delete medium"), clone!(@weak this => move || {
|
||||
// TODO: Delete medium and maybe also the tracks?
|
||||
}));
|
||||
|
||||
section.add_action("media-playback-start-symbolic", clone!(@weak this => move || {
|
||||
for track_set in &this.medium.tracks {
|
||||
let indices = (0..track_set.tracks.len()).collect();
|
||||
|
||||
let playlist_item = PlaylistItem {
|
||||
track_set: track_set.clone(),
|
||||
indices,
|
||||
};
|
||||
|
||||
this.handle.backend.pl().add_item(playlist_item).unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
match this.items[index] {
|
||||
ListItem::Header(index) => {
|
||||
let track_set = &this.medium.tracks[index];
|
||||
let recording = &track_set.recording;
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(false);
|
||||
row.set_selectable(false);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
ListItem::Track(track_set_index, track_index) => {
|
||||
let track_set = &this.medium.tracks[track_set_index];
|
||||
let track = &track_set.tracks[track_index];
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let title = if parts.is_empty() {
|
||||
gettext("Unknown")
|
||||
} else {
|
||||
parts.join(", ")
|
||||
};
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_selectable(false);
|
||||
row.set_activatable(false);
|
||||
row.set_title(Some(&title));
|
||||
row.set_margin_start(12);
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
ListItem::Separator => {
|
||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
||||
separator.upcast()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.list.update(this.items.len());
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MediumScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
pub mod ensemble;
|
||||
pub use ensemble::*;
|
||||
|
||||
pub mod main;
|
||||
pub use main::*;
|
||||
|
||||
pub mod medium;
|
||||
pub use medium::*;
|
||||
|
||||
pub mod person;
|
||||
pub use person::*;
|
||||
|
||||
pub mod player;
|
||||
pub use player::*;
|
||||
|
||||
pub mod work;
|
||||
pub use work::*;
|
||||
|
||||
pub mod welcome;
|
||||
pub use welcome::*;
|
||||
|
||||
pub mod recording;
|
||||
pub use recording::*;
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
use super::{MediumScreen, WorkScreen, RecordingScreen};
|
||||
use crate::editors::PersonEditor;
|
||||
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
|
||||
use crate::widgets;
|
||||
use crate::widgets::{List, Section, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{Medium, Person, Recording, Work};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for showing works by and recordings with a person.
|
||||
pub struct PersonScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
person: Person,
|
||||
widget: widgets::Screen,
|
||||
work_list: Rc<List>,
|
||||
recording_list: Rc<List>,
|
||||
medium_list: Rc<List>,
|
||||
works: RefCell<Vec<Work>>,
|
||||
recordings: RefCell<Vec<Recording>>,
|
||||
mediums: RefCell<Vec<Medium>>,
|
||||
}
|
||||
|
||||
impl Screen<Person, ()> for PersonScreen {
|
||||
/// Create a new person screen for the specified person and load the
|
||||
/// contents asynchronously.
|
||||
fn new(person: Person, handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let widget = widgets::Screen::new();
|
||||
widget.set_title(&person.name_fl());
|
||||
|
||||
let work_list = List::new();
|
||||
let recording_list = List::new();
|
||||
let medium_list = List::new();
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
person,
|
||||
widget,
|
||||
work_list,
|
||||
recording_list,
|
||||
medium_list,
|
||||
works: RefCell::new(Vec::new()),
|
||||
recordings: RefCell::new(Vec::new()),
|
||||
mediums: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
this.widget.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
|
||||
this.widget.add_action(&gettext("Edit person"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
let window = NavigatorWindow::new(this.handle.backend.clone());
|
||||
replace!(window.navigator, PersonEditor, Some(this.person.clone())).await;
|
||||
});
|
||||
}));
|
||||
|
||||
this.widget.add_action(&gettext("Delete person"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.db().delete_person(&this.person.id).await.unwrap();
|
||||
this.handle.backend.library_changed();
|
||||
});
|
||||
}));
|
||||
|
||||
this.widget.set_search_cb(clone!(@weak this => move || {
|
||||
this.work_list.invalidate_filter();
|
||||
this.recording_list.invalidate_filter();
|
||||
this.medium_list.invalidate_filter();
|
||||
}));
|
||||
|
||||
this.work_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let work = &this.works.borrow()[index];
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&work.title));
|
||||
|
||||
let work = work.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
let work = work.clone();
|
||||
spawn!(@clone this, async move {
|
||||
push!(this.handle, WorkScreen, work.clone()).await;
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.work_list.set_filter_cb(clone!(@weak this => move |index| {
|
||||
let work = &this.works.borrow()[index];
|
||||
let search = this.widget.get_search();
|
||||
let title = work.title.to_lowercase();
|
||||
search.is_empty() || title.contains(&search)
|
||||
}));
|
||||
|
||||
this.recording_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
let recording = recording.clone();
|
||||
spawn!(@clone this, async move {
|
||||
push!(this.handle, RecordingScreen, recording.clone()).await;
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.recording_list.set_filter_cb(clone!(@weak this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
let search = this.widget.get_search();
|
||||
let text = recording.work.get_title() + &recording.get_performers();
|
||||
search.is_empty() || text.to_lowercase().contains(&search)
|
||||
}));
|
||||
|
||||
this.medium_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let medium = &this.mediums.borrow()[index];
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&medium.name));
|
||||
|
||||
let medium = medium.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
let medium = medium.clone();
|
||||
spawn!(@clone this, async move {
|
||||
push!(this.handle, MediumScreen, medium.clone()).await;
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.medium_list.set_filter_cb(clone!(@weak this => move |index| {
|
||||
let medium = &this.mediums.borrow()[index];
|
||||
let search = this.widget.get_search();
|
||||
let name = medium.name.to_lowercase();
|
||||
search.is_empty() || name.contains(&search)
|
||||
}));
|
||||
|
||||
// Load the content asynchronously.
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let works = this.handle
|
||||
.backend
|
||||
.db()
|
||||
.get_works(&this.person.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let recordings = this.handle
|
||||
.backend
|
||||
.db()
|
||||
.get_recordings_for_person(&this.person.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mediums = this.handle
|
||||
.backend
|
||||
.db()
|
||||
.get_mediums_for_person(&this.person.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !works.is_empty() {
|
||||
let length = works.len();
|
||||
this.works.replace(works);
|
||||
this.work_list.update(length);
|
||||
|
||||
let section = Section::new("Works", &this.work_list.widget);
|
||||
this.widget.add_content(§ion.widget);
|
||||
}
|
||||
|
||||
if !recordings.is_empty() {
|
||||
let length = recordings.len();
|
||||
this.recordings.replace(recordings);
|
||||
this.recording_list.update(length);
|
||||
|
||||
let section = Section::new("Recordings", &this.recording_list.widget);
|
||||
this.widget.add_content(§ion.widget);
|
||||
}
|
||||
|
||||
if !mediums.is_empty() {
|
||||
let length = mediums.len();
|
||||
this.mediums.replace(mediums);
|
||||
this.medium_list.update(length);
|
||||
|
||||
let section = Section::new("Mediums", &this.medium_list.widget);
|
||||
this.widget.add_content(§ion.widget);
|
||||
}
|
||||
|
||||
this.widget.ready();
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PersonScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{List, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::PlaylistItem;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Elements for visually representing the playlist.
|
||||
enum ListItem {
|
||||
/// A header shown on top of a track set. This contains an index
|
||||
/// referencing the playlist item containing this track set.
|
||||
Header(usize),
|
||||
|
||||
/// A playable track. This contains an index to the playlist item, an
|
||||
/// index to the track and whether it is the currently played one.
|
||||
Track(usize, usize, bool),
|
||||
|
||||
/// A separator shown between track sets.
|
||||
Separator,
|
||||
}
|
||||
|
||||
pub struct PlayerScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
widget: gtk::Box,
|
||||
title_label: gtk::Label,
|
||||
subtitle_label: gtk::Label,
|
||||
previous_button: gtk::Button,
|
||||
play_button: gtk::Button,
|
||||
next_button: gtk::Button,
|
||||
position_label: gtk::Label,
|
||||
position: gtk::Adjustment,
|
||||
duration_label: gtk::Label,
|
||||
play_image: gtk::Image,
|
||||
pause_image: gtk::Image,
|
||||
list: Rc<List>,
|
||||
playlist: RefCell<Vec<PlaylistItem>>,
|
||||
items: RefCell<Vec<ListItem>>,
|
||||
seeking: Cell<bool>,
|
||||
current_item: Cell<usize>,
|
||||
current_track: Cell<usize>,
|
||||
}
|
||||
|
||||
impl Screen<(), ()> for PlayerScreen {
|
||||
fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Label, subtitle_label);
|
||||
get_widget!(builder, gtk::Button, previous_button);
|
||||
get_widget!(builder, gtk::Button, play_button);
|
||||
get_widget!(builder, gtk::Button, next_button);
|
||||
get_widget!(builder, gtk::Button, stop_button);
|
||||
get_widget!(builder, gtk::Label, position_label);
|
||||
get_widget!(builder, gtk::Scale, position_scale);
|
||||
get_widget!(builder, gtk::Adjustment, position);
|
||||
get_widget!(builder, gtk::Label, duration_label);
|
||||
get_widget!(builder, gtk::Image, play_image);
|
||||
get_widget!(builder, gtk::Image, pause_image);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
let position_controller = gtk::GestureClick::new();
|
||||
position_scale.add_controller(&position_controller);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
title_label,
|
||||
subtitle_label,
|
||||
previous_button,
|
||||
play_button,
|
||||
next_button,
|
||||
position_label,
|
||||
position,
|
||||
duration_label,
|
||||
play_image,
|
||||
pause_image,
|
||||
list,
|
||||
items: RefCell::new(Vec::new()),
|
||||
playlist: RefCell::new(Vec::new()),
|
||||
seeking: Cell::new(false),
|
||||
current_item: Cell::new(0),
|
||||
current_track: Cell::new(0),
|
||||
});
|
||||
|
||||
let player = &this.handle.backend.pl();
|
||||
|
||||
player.add_playlist_cb(clone!(@weak this => @default-return (), move |playlist| {
|
||||
if playlist.is_empty() {
|
||||
this.handle.pop(None);
|
||||
}
|
||||
|
||||
this.playlist.replace(playlist);
|
||||
this.show_playlist();
|
||||
}));
|
||||
|
||||
player.add_track_cb(clone!(@weak this, @weak player => @default-return (), move |current_item, current_track| {
|
||||
this.previous_button.set_sensitive(this.handle.backend.pl().has_previous());
|
||||
this.next_button.set_sensitive(this.handle.backend.pl().has_next());
|
||||
|
||||
let item = &this.playlist.borrow()[current_item];
|
||||
let track = &item.track_set.tracks[current_track];
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let mut title = item.track_set.recording.work.get_title();
|
||||
if !parts.is_empty() {
|
||||
title = format!("{}: {}", title, parts.join(", "));
|
||||
}
|
||||
|
||||
this.title_label.set_text(&title);
|
||||
this.subtitle_label.set_text(&item.track_set.recording.get_performers());
|
||||
this.position_label.set_text("0:00");
|
||||
|
||||
this.current_item.set(current_item);
|
||||
this.current_track.set(current_track);
|
||||
|
||||
this.show_playlist();
|
||||
}));
|
||||
|
||||
player.add_duration_cb(clone!(@weak this => @default-return (), move |ms| {
|
||||
let min = ms / 60000;
|
||||
let sec = (ms % 60000) / 1000;
|
||||
this.duration_label.set_text(&format!("{}:{:02}", min, sec));
|
||||
this.position.set_upper(ms as f64);
|
||||
}));
|
||||
|
||||
player.add_playing_cb(clone!(@weak this => @default-return (), move |playing| {
|
||||
this.play_button.set_child(Some(if playing {
|
||||
&this.pause_image
|
||||
} else {
|
||||
&this.play_image
|
||||
}));
|
||||
}));
|
||||
|
||||
player.add_position_cb(clone!(@weak this => @default-return (), move |ms| {
|
||||
if !this.seeking.get() {
|
||||
let min = ms / 60000;
|
||||
let sec = (ms % 60000) / 1000;
|
||||
this.position_label.set_text(&format!("{}:{:02}", min, sec));
|
||||
this.position.set_value(ms as f64);
|
||||
}
|
||||
}));
|
||||
|
||||
back_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.previous_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.backend.pl().previous().unwrap();
|
||||
}));
|
||||
|
||||
this.play_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.backend.pl().play_pause();
|
||||
}));
|
||||
|
||||
this.next_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.backend.pl().next().unwrap();
|
||||
}));
|
||||
|
||||
stop_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.backend.pl().clear();
|
||||
}));
|
||||
|
||||
// position_controller.connect_pressed(clone!(@weak this => move |_, _, _, _| {
|
||||
// this.seeking.replace(true);
|
||||
// }));
|
||||
|
||||
// position_controller.connect_unpaired_release(clone!(@weak this => move |_, _, _, _, _| {
|
||||
// this.handle.backend.pl().seek(this.position.get_value() as u64);
|
||||
// this.seeking.replace(false);
|
||||
// }));
|
||||
|
||||
// position_scale.connect_value_changed(clone!(@weak this => move |_| {
|
||||
// if this.seeking.get() {
|
||||
// let ms = this.position.get_value() as u64;
|
||||
// let min = ms / 60000;
|
||||
// let sec = (ms % 60000) / 1000;
|
||||
|
||||
// this.position_label.set_text(&format!("{}:{:02}", min, sec));
|
||||
// }
|
||||
// }));
|
||||
|
||||
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
match this.items.borrow()[index] {
|
||||
ListItem::Header(item_index) => {
|
||||
let playlist_item = &this.playlist.borrow()[item_index];
|
||||
let recording = &playlist_item.track_set.recording;
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(false);
|
||||
row.set_selectable(false);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
ListItem::Track(item_index, track_index, playing) => {
|
||||
let playlist_item = &this.playlist.borrow()[item_index];
|
||||
let index = playlist_item.indices[track_index];
|
||||
let track = &playlist_item.track_set.tracks[index];
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(playlist_item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let title = if parts.is_empty() {
|
||||
gettext("Unknown")
|
||||
} else {
|
||||
parts.join(", ")
|
||||
};
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_selectable(false);
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&title));
|
||||
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
this.handle.backend.pl().set_track(item_index, track_index).unwrap();
|
||||
}));
|
||||
|
||||
let icon = if playing {
|
||||
Some("media-playback-start-symbolic")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let image = gtk::Image::from_icon_name(icon);
|
||||
row.add_prefix(&image);
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
ListItem::Separator => {
|
||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
||||
separator.upcast()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
player.send_data();
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerScreen {
|
||||
/// Update the user interface according to the playlist.
|
||||
fn show_playlist(&self) {
|
||||
let playlist = self.playlist.borrow();
|
||||
let current_item = self.current_item.get();
|
||||
let current_track = self.current_track.get();
|
||||
|
||||
let mut first = true;
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (item_index, playlist_item) in playlist.iter().enumerate() {
|
||||
if !first {
|
||||
items.push(ListItem::Separator);
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
items.push(ListItem::Header(item_index));
|
||||
|
||||
for (index, _) in playlist_item.indices.iter().enumerate() {
|
||||
let playing = current_item == item_index && current_track == index;
|
||||
items.push(ListItem::Track(item_index, index, playing));
|
||||
}
|
||||
}
|
||||
|
||||
let length = items.len();
|
||||
self.items.replace(items);
|
||||
self.list.update(length);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PlayerScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
use crate::editors::RecordingEditor;
|
||||
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
|
||||
use crate::widgets;
|
||||
use crate::widgets::{List, Section, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::PlaylistItem;
|
||||
use musicus_backend::db::{Recording, TrackSet};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Representation of one entry within the track list.
|
||||
enum ListItem {
|
||||
/// A track row. This hold an index to the track set and an index to the
|
||||
/// track within the track set.
|
||||
Track(usize, usize),
|
||||
|
||||
/// A separator intended for use between track sets.
|
||||
Separator,
|
||||
}
|
||||
|
||||
/// A screen for showing a recording.
|
||||
pub struct RecordingScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
recording: Recording,
|
||||
widget: widgets::Screen,
|
||||
list: Rc<List>,
|
||||
track_sets: RefCell<Vec<TrackSet>>,
|
||||
items: RefCell<Vec<ListItem>>,
|
||||
}
|
||||
|
||||
impl Screen<Recording, ()> for RecordingScreen {
|
||||
/// Create a new recording screen for the specified recording and load the
|
||||
/// contents asynchronously.
|
||||
fn new(recording: Recording, handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let widget = widgets::Screen::new();
|
||||
widget.set_title(&recording.work.get_title());
|
||||
widget.set_subtitle(&recording.get_performers());
|
||||
|
||||
let list = List::new();
|
||||
let section = Section::new(&gettext("Tracks"), &list.widget);
|
||||
widget.add_content(§ion.widget);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
recording,
|
||||
widget,
|
||||
list,
|
||||
track_sets: RefCell::new(Vec::new()),
|
||||
items: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
section.add_action("media-playback-start-symbolic", clone!(@weak this => move || {
|
||||
if let Some(player) = this.handle.backend.get_player() {
|
||||
if let Some(track_set) = this.track_sets.borrow().get(0).cloned() {
|
||||
let indices = (0..track_set.tracks.len()).collect();
|
||||
|
||||
let playlist_item = PlaylistItem {
|
||||
track_set,
|
||||
indices,
|
||||
};
|
||||
|
||||
player.add_item(playlist_item).unwrap();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.widget.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.widget.add_action(&gettext("Edit recording"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
let window = NavigatorWindow::new(this.handle.backend.clone());
|
||||
replace!(window.navigator, RecordingEditor, Some(this.recording.clone())).await;
|
||||
});
|
||||
}));
|
||||
|
||||
this.widget.add_action(&gettext("Delete recording"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.db().delete_recording(&this.recording.id).await.unwrap();
|
||||
this.handle.backend.library_changed();
|
||||
});
|
||||
}));
|
||||
|
||||
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
match this.items.borrow()[index] {
|
||||
ListItem::Track(track_set_index, track_index) => {
|
||||
let track_set = &this.track_sets.borrow()[track_set_index];
|
||||
let track = &track_set.tracks[track_index];
|
||||
|
||||
let mut title_parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
title_parts.push(this.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let title = if title_parts.is_empty() {
|
||||
gettext("Unknown")
|
||||
} else {
|
||||
title_parts.join(", ")
|
||||
};
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_title(Some(&title));
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
ListItem::Separator => {
|
||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
||||
separator.upcast()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Load the content asynchronously.
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let track_sets = this.handle
|
||||
.backend
|
||||
.db()
|
||||
.get_track_sets(&this.recording.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
this.show_track_sets(track_sets);
|
||||
this.widget.ready();
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl RecordingScreen {
|
||||
/// Update the track sets variable as well as the user interface.
|
||||
fn show_track_sets(&self, track_sets: Vec<TrackSet>) {
|
||||
let mut first = true;
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (track_set_index, track_set) in track_sets.iter().enumerate() {
|
||||
if !first {
|
||||
items.push(ListItem::Separator);
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
for (track_index, _) in track_set.tracks.iter().enumerate() {
|
||||
items.push(ListItem::Track(track_set_index, track_index));
|
||||
}
|
||||
}
|
||||
|
||||
let length = items.len();
|
||||
self.items.replace(items);
|
||||
self.track_sets.replace(track_sets);
|
||||
self.list.update(length);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RecordingScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen displaying a welcome message and the necessary means to set up the application. This
|
||||
/// screen doesn't access the backend except for setting the initial values and is safe to be used
|
||||
/// while the backend is loading.
|
||||
pub struct WelcomeScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
widget: gtk::Box,
|
||||
}
|
||||
|
||||
impl Screen<(), ()> for WelcomeScreen {
|
||||
fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let widget = gtk::BoxBuilder::new()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
|
||||
let header = libadwaita::HeaderBarBuilder::new()
|
||||
.title_widget(&libadwaita::WindowTitle::new(Some("Musicus"), None))
|
||||
.build();
|
||||
|
||||
let button = gtk::ButtonBuilder::new()
|
||||
.halign(gtk::Align::Center)
|
||||
.label(&gettext("Select folder"))
|
||||
.build();
|
||||
|
||||
let welcome = libadwaita::StatusPageBuilder::new()
|
||||
.icon_name("folder-music-symbolic")
|
||||
.title(&gettext("Welcome to Musicus!"))
|
||||
.description(&gettext("Get startet by selecting the folder containing your music \
|
||||
files! Musicus will create a new database there or open one that already exists."))
|
||||
.child(&button)
|
||||
.build();
|
||||
|
||||
button.add_css_class("suggested-action");
|
||||
|
||||
widget.append(&header);
|
||||
widget.append(&welcome);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
});
|
||||
|
||||
button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let dialog = gtk::FileChooserDialog::new(
|
||||
Some(&gettext("Select music library folder")),
|
||||
Some(&this.handle.window),
|
||||
gtk::FileChooserAction::SelectFolder,
|
||||
&[
|
||||
(&gettext("Cancel"), gtk::ResponseType::Cancel),
|
||||
(&gettext("Select"), gtk::ResponseType::Accept),
|
||||
]);
|
||||
|
||||
dialog.set_modal(true);
|
||||
|
||||
dialog.connect_response(clone!(@weak this => move |dialog, response| {
|
||||
if let gtk::ResponseType::Accept = response {
|
||||
if let Some(file) = dialog.get_file() {
|
||||
if let Some(path) = file.get_path() {
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.set_music_library_path(path).await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
}));
|
||||
|
||||
dialog.show();
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WelcomeScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
use super::RecordingScreen;
|
||||
use crate::editors::WorkEditor;
|
||||
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
|
||||
use crate::widgets;
|
||||
use crate::widgets::{List, Section, Widget};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{Work, Recording};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for showing recordings of a work.
|
||||
pub struct WorkScreen {
|
||||
handle: NavigationHandle<()>,
|
||||
work: Work,
|
||||
widget: widgets::Screen,
|
||||
recording_list: Rc<List>,
|
||||
recordings: RefCell<Vec<Recording>>,
|
||||
}
|
||||
|
||||
impl Screen<Work, ()> for WorkScreen {
|
||||
/// Create a new work screen for the specified work and load the
|
||||
/// contents asynchronously.
|
||||
fn new(work: Work, handle: NavigationHandle<()>) -> Rc<Self> {
|
||||
let widget = widgets::Screen::new();
|
||||
widget.set_title(&work.title);
|
||||
widget.set_subtitle(&work.composer.name_fl());
|
||||
|
||||
let recording_list = List::new();
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
work,
|
||||
widget,
|
||||
recording_list,
|
||||
recordings: RefCell::new(Vec::new()),
|
||||
});
|
||||
|
||||
this.widget.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
|
||||
this.widget.add_action(&gettext("Edit work"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
let window = NavigatorWindow::new(this.handle.backend.clone());
|
||||
replace!(window.navigator, WorkEditor, Some(this.work.clone())).await;
|
||||
});
|
||||
}));
|
||||
|
||||
this.widget.add_action(&gettext("Delete work"), clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.db().delete_work(&this.work.id).await.unwrap();
|
||||
this.handle.backend.library_changed();
|
||||
});
|
||||
}));
|
||||
|
||||
this.widget.set_search_cb(clone!(@weak this => move || {
|
||||
this.recording_list.invalidate_filter();
|
||||
}));
|
||||
|
||||
this.recording_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
let recording = recording.clone();
|
||||
spawn!(@clone this, async move {
|
||||
push!(this.handle, RecordingScreen, recording.clone()).await;
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.recording_list.set_filter_cb(clone!(@weak this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
let search = this.widget.get_search();
|
||||
let text = recording.work.get_title() + &recording.get_performers();
|
||||
search.is_empty() || text.to_lowercase().contains(&search)
|
||||
}));
|
||||
|
||||
// Load the content asynchronously.
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let recordings = this.handle
|
||||
.backend
|
||||
.db()
|
||||
.get_recordings_for_work(&this.work.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !recordings.is_empty() {
|
||||
let length = recordings.len();
|
||||
this.recordings.replace(recordings);
|
||||
this.recording_list.update(length);
|
||||
|
||||
let section = Section::new("Recordings", &this.recording_list.widget);
|
||||
this.widget.add_content(§ion.widget);
|
||||
}
|
||||
|
||||
this.widget.ready();
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WorkScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
use super::selector::Selector;
|
||||
use crate::editors::EnsembleEditor;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::Ensemble;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for selecting a ensemble.
|
||||
pub struct EnsembleSelector {
|
||||
handle: NavigationHandle<Ensemble>,
|
||||
selector: Rc<Selector<Ensemble>>,
|
||||
}
|
||||
|
||||
impl Screen<(), Ensemble> for EnsembleSelector {
|
||||
/// Create a new ensemble selector.
|
||||
fn new(_: (), handle: NavigationHandle<Ensemble>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Ensemble>::new();
|
||||
selector.set_title(&gettext("Select ensemble"));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
selector,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(ensemble) = push!(this.handle, EnsembleEditor, None).await {
|
||||
this.handle.pop(Some(ensemble));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
let clone = this.clone();
|
||||
async move { Ok(clone.handle.backend.cl().get_ensembles().await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
let clone = this.clone();
|
||||
async move { clone.handle.backend.db().get_ensembles().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |ensemble| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&ensemble.name));
|
||||
|
||||
let ensemble = ensemble.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
this.handle.pop(Some(ensemble.clone()))
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, ensemble| ensemble.name.to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for EnsembleSelector {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
use super::selector::Selector;
|
||||
use crate::editors::InstrumentEditor;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::Instrument;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for selecting a instrument.
|
||||
pub struct InstrumentSelector {
|
||||
handle: NavigationHandle<Instrument>,
|
||||
selector: Rc<Selector<Instrument>>,
|
||||
}
|
||||
|
||||
impl Screen<(), Instrument> for InstrumentSelector {
|
||||
/// Create a new instrument selector.
|
||||
fn new(_: (), handle: NavigationHandle<Instrument>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Instrument>::new();
|
||||
selector.set_title(&gettext("Select instrument"));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
selector,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(instrument) = push!(this.handle, InstrumentEditor, None).await {
|
||||
this.handle.pop(Some(instrument));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
let clone = this.clone();
|
||||
async move { Ok(clone.handle.backend.cl().get_instruments().await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
let clone = this.clone();
|
||||
async move { clone.handle.backend.db().get_instruments().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |instrument| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&instrument.name));
|
||||
|
||||
let instrument = instrument.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
this.handle.pop(Some(instrument.clone()))
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, instrument| instrument.name.to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for InstrumentSelector {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +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 selector;
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
use super::selector::Selector;
|
||||
use crate::editors::PersonEditor;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::Person;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for selecting a person.
|
||||
pub struct PersonSelector {
|
||||
handle: NavigationHandle<Person>,
|
||||
selector: Rc<Selector<Person>>,
|
||||
}
|
||||
|
||||
impl Screen<(), Person> for PersonSelector {
|
||||
/// Create a new person selector.
|
||||
fn new(_: (), handle: NavigationHandle<Person>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Person>::new();
|
||||
selector.set_title(&gettext("Select person"));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
selector,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(person) = push!(this.handle, PersonEditor, None).await {
|
||||
this.handle.pop(Some(person));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
let clone = this.clone();
|
||||
async move { Ok(clone.handle.backend.cl().get_persons().await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
let clone = this.clone();
|
||||
async move { clone.handle.backend.db().get_persons().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |person| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&person.name_lf()));
|
||||
|
||||
let person = person.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
this.handle.pop(Some(person.clone()));
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for PersonSelector {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
use super::selector::Selector;
|
||||
use crate::editors::{PersonEditor, WorkEditor, RecordingEditor};
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{Person, Work, Recording};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for selecting a recording.
|
||||
pub struct RecordingSelector {
|
||||
handle: NavigationHandle<Recording>,
|
||||
selector: Rc<Selector<Person>>,
|
||||
}
|
||||
|
||||
impl Screen<(), Recording> for RecordingSelector {
|
||||
fn new(_: (), handle: NavigationHandle<Recording>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Person>::new();
|
||||
selector.set_title(&gettext("Select composer"));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
selector,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(person) = push!(this.handle, PersonEditor, None).await {
|
||||
// We can assume that there are no existing works of this composer and
|
||||
// immediately show the work editor. Going back from the work editor will
|
||||
// correctly show the person selector again.
|
||||
|
||||
let work = Work::new(person);
|
||||
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
|
||||
// There will also be no existing recordings, so we show the recording
|
||||
// editor next.
|
||||
|
||||
let recording = Recording::new(work);
|
||||
if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await {
|
||||
this.handle.pop(Some(recording));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
async move { Ok(this.handle.backend.cl().get_persons().await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
async move { this.handle.backend.db().get_persons().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |person| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&person.name_lf()));
|
||||
|
||||
let person = person.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
// Instead of returning the person from here, like the person selector does, we
|
||||
// show a second selector for choosing the work.
|
||||
|
||||
let person = person.clone();
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(work) = push!(this.handle, RecordingSelectorWorkScreen, person).await {
|
||||
// Now the user can select a recording for that work.
|
||||
|
||||
if let Some(recording) = push!(this.handle, RecordingSelectorRecordingScreen, work).await {
|
||||
this.handle.pop(Some(recording));
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RecordingSelector {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
/// The work selector within the recording selector.
|
||||
struct RecordingSelectorWorkScreen {
|
||||
handle: NavigationHandle<Work>,
|
||||
person: Person,
|
||||
selector: Rc<Selector<Work>>,
|
||||
}
|
||||
|
||||
impl Screen<Person, Work> for RecordingSelectorWorkScreen {
|
||||
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> {
|
||||
let selector = Selector::<Work>::new();
|
||||
selector.set_title(&gettext("Select work"));
|
||||
selector.set_subtitle(&person.name_fl());
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
person,
|
||||
selector,
|
||||
});
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
let work = Work::new(this.person.clone());
|
||||
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
|
||||
this.handle.pop(Some(work));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
async move { Ok(this.handle.backend.cl().get_works(&this.person.id).await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |work| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&work.title));
|
||||
|
||||
let work = work.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
this.handle.pop(Some(work.clone()));
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector.set_filter(|search, work| work.title.to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RecordingSelectorWorkScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual recording selector within the recording selector.
|
||||
struct RecordingSelectorRecordingScreen {
|
||||
handle: NavigationHandle<Recording>,
|
||||
work: Work,
|
||||
selector: Rc<Selector<Recording>>,
|
||||
}
|
||||
|
||||
impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
|
||||
fn new(work: Work, handle: NavigationHandle<Recording>) -> Rc<Self> {
|
||||
let selector = Selector::<Recording>::new();
|
||||
selector.set_title(&gettext("Select recording"));
|
||||
selector.set_subtitle(&work.get_title());
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
work,
|
||||
selector,
|
||||
});
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
let recording = Recording::new(this.work.clone());
|
||||
if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await {
|
||||
this.handle.pop(Some(recording));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
async move { Ok(this.handle.backend.cl().get_recordings_for_work(&this.work.id).await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
async move { this.handle.backend.db().get_recordings_for_work(&this.work.id).await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |recording| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
this.handle.pop(Some(recording.clone()));
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, recording| recording.get_performers().to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RecordingSelectorRecordingScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
use crate::widgets::List;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::Result;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen that presents a list of items. It allows to switch between the server and the local
|
||||
/// database and to search within the list.
|
||||
pub struct Selector<T: 'static> {
|
||||
pub widget: gtk::Box,
|
||||
title_label: gtk::Label,
|
||||
subtitle_label: gtk::Label,
|
||||
search_entry: gtk::SearchEntry,
|
||||
server_check_button: gtk::CheckButton,
|
||||
stack: gtk::Stack,
|
||||
list: Rc<List>,
|
||||
items: RefCell<Vec<T>>,
|
||||
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
|
||||
load_online: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Result<Vec<T>>>>>>>,
|
||||
load_local: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Vec<T>>>>>>,
|
||||
filter: RefCell<Option<Box<dyn Fn(&str, &T) -> bool>>>,
|
||||
}
|
||||
|
||||
impl<T> Selector<T> {
|
||||
/// Create a new selector.
|
||||
pub fn new() -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/selector.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Label, subtitle_label);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Button, add_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
get_widget!(builder, gtk::CheckButton, server_check_button);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
title_label,
|
||||
subtitle_label,
|
||||
search_entry,
|
||||
server_check_button,
|
||||
stack,
|
||||
list,
|
||||
items: RefCell::new(Vec::new()),
|
||||
back_cb: RefCell::new(None),
|
||||
add_cb: RefCell::new(None),
|
||||
make_widget: RefCell::new(None),
|
||||
load_online: RefCell::new(None),
|
||||
load_local: RefCell::new(None),
|
||||
filter: RefCell::new(None),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(cb) = &*this.back_cb.borrow() {
|
||||
cb();
|
||||
}
|
||||
}));
|
||||
|
||||
add_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(cb) = &*this.add_cb.borrow() {
|
||||
cb();
|
||||
}
|
||||
}));
|
||||
|
||||
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
|
||||
this.list.invalidate_filter();
|
||||
}));
|
||||
|
||||
this.server_check_button
|
||||
.connect_toggled(clone!(@strong this => move |_| {
|
||||
if this.server_check_button.get_active() {
|
||||
this.clone().load_online();
|
||||
} else {
|
||||
this.clone().load_local();
|
||||
}
|
||||
}));
|
||||
|
||||
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
if let Some(cb) = &*this.make_widget.borrow() {
|
||||
let item = &this.items.borrow()[index];
|
||||
cb(item)
|
||||
} else {
|
||||
gtk::Label::new(None).upcast()
|
||||
}
|
||||
}));
|
||||
|
||||
this.list.set_filter_cb(clone!(@strong this => move |index| {
|
||||
match &*this.filter.borrow() {
|
||||
Some(filter) => {
|
||||
let item = &this.items.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
search.is_empty() || filter(&search, item)
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}));
|
||||
|
||||
try_again_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
this.clone().load_online();
|
||||
}));
|
||||
|
||||
// Initialize
|
||||
this.clone().load_online();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// Set the title to be shown in the header.
|
||||
pub fn set_title(&self, title: &str) {
|
||||
self.title_label.set_label(&title);
|
||||
}
|
||||
|
||||
/// Set the subtitle to be shown in the header.
|
||||
pub fn set_subtitle(&self, subtitle: &str) {
|
||||
self.subtitle_label.set_label(&subtitle);
|
||||
self.subtitle_label.show();
|
||||
}
|
||||
|
||||
/// Set the closure to be called when the user wants to go back.
|
||||
pub fn set_back_cb<F: Fn() -> () + 'static>(&self, cb: F) {
|
||||
self.back_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Set the closure to be called when the user wants to add an item.
|
||||
pub fn set_add_cb<F: Fn() -> () + 'static>(&self, cb: F) {
|
||||
self.add_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Set the async closure to be called to fetch items from the server. If that results in an
|
||||
/// error, an error screen is shown allowing to try again.
|
||||
pub fn set_load_online<F, R>(&self, cb: F)
|
||||
where
|
||||
F: (Fn() -> R) + 'static,
|
||||
R: Future<Output = Result<Vec<T>>> + 'static,
|
||||
{
|
||||
self.load_online
|
||||
.replace(Some(Box::new(move || Box::new(cb()))));
|
||||
}
|
||||
|
||||
/// Set the async closure to be called to get local items.
|
||||
pub fn set_load_local<F, R>(&self, cb: F)
|
||||
where
|
||||
F: (Fn() -> R) + 'static,
|
||||
R: Future<Output = Vec<T>> + 'static,
|
||||
{
|
||||
self.load_local
|
||||
.replace(Some(Box::new(move || Box::new(cb()))));
|
||||
}
|
||||
|
||||
/// Set the closure to be called for creating a new list row.
|
||||
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
||||
self.make_widget.replace(Some(Box::new(make_widget)));
|
||||
}
|
||||
|
||||
/// Set a closure to call when deciding whether to show an item based on a search string. The
|
||||
/// search string will be converted to lowercase.
|
||||
pub fn set_filter<F: Fn(&str, &T) -> bool + 'static>(&self, filter: F) {
|
||||
self.filter.replace(Some(Box::new(filter)));
|
||||
}
|
||||
|
||||
fn load_online(self: Rc<Self>) {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = self.clone();
|
||||
context.spawn_local(async move {
|
||||
if let Some(cb) = &*self.load_online.borrow() {
|
||||
self.stack.set_visible_child_name("loading");
|
||||
|
||||
match Pin::from(cb()).await {
|
||||
Ok(items) => {
|
||||
clone.show_items(items);
|
||||
}
|
||||
Err(_) => {
|
||||
clone.show_items(Vec::new());
|
||||
clone.stack.set_visible_child_name("error");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn load_local(self: Rc<Self>) {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = self.clone();
|
||||
context.spawn_local(async move {
|
||||
if let Some(cb) = &*self.load_local.borrow() {
|
||||
self.stack.set_visible_child_name("loading");
|
||||
|
||||
let items = Pin::from(cb()).await;
|
||||
clone.show_items(items);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_items(&self, items: Vec<T>) {
|
||||
let length = items.len();
|
||||
self.items.replace(items);
|
||||
self.list.update(length);
|
||||
self.stack.set_visible_child_name("content");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
use super::selector::Selector;
|
||||
use crate::editors::{PersonEditor, WorkEditor};
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use musicus_backend::db::{Person, Work};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen for selecting a work.
|
||||
pub struct WorkSelector {
|
||||
handle: NavigationHandle<Work>,
|
||||
selector: Rc<Selector<Person>>,
|
||||
}
|
||||
|
||||
impl Screen<(), Work> for WorkSelector {
|
||||
fn new(_: (), handle: NavigationHandle<Work>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Person>::new();
|
||||
selector.set_title(&gettext("Select composer"));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
selector,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(person) = push!(this.handle, PersonEditor, None).await {
|
||||
// We can assume that there are no existing works of this composer and
|
||||
// immediately show the work editor. Going back from the work editor will
|
||||
// correctly show the person selector again.
|
||||
|
||||
let work = Work::new(person);
|
||||
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
|
||||
this.handle.pop(Some(work));
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
async move { Ok(this.handle.backend.cl().get_persons().await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
async move { this.handle.backend.db().get_persons().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |person| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&person.name_lf()));
|
||||
|
||||
let person = person.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
// Instead of returning the person from here, like the person selector does, we
|
||||
// show a second selector for choosing the work.
|
||||
|
||||
let person = person.clone();
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(work) = push!(this.handle, WorkSelectorWorkScreen, person).await {
|
||||
this.handle.pop(Some(work));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WorkSelector {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual work selector that is displayed after the user has selected a composer.
|
||||
struct WorkSelectorWorkScreen {
|
||||
handle: NavigationHandle<Work>,
|
||||
person: Person,
|
||||
selector: Rc<Selector<Work>>,
|
||||
}
|
||||
|
||||
impl Screen<Person, Work> for WorkSelectorWorkScreen {
|
||||
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> {
|
||||
let selector = Selector::<Work>::new();
|
||||
selector.set_title(&gettext("Select work"));
|
||||
selector.set_subtitle(&person.name_fl());
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
person,
|
||||
selector,
|
||||
});
|
||||
|
||||
this.selector.set_back_cb(clone!(@weak this => move || {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector.set_add_cb(clone!(@weak this => move || {
|
||||
spawn!(@clone this, async move {
|
||||
let work = Work::new(this.person.clone());
|
||||
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
|
||||
this.handle.pop(Some(work));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => move || {
|
||||
async move { Ok(this.handle.backend.cl().get_works(&this.person.id).await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => move || {
|
||||
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(clone!(@weak this => move |work| {
|
||||
let row = libadwaita::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&work.title));
|
||||
|
||||
let work = work.to_owned();
|
||||
row.connect_activated(clone!(@weak this => move |_| {
|
||||
this.handle.pop(Some(work.clone()));
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector.set_filter(|search, work| work.title.to_lowercase().contains(search));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WorkSelectorWorkScreen {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.selector.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
use super::Widget;
|
||||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
|
||||
/// A list box row with a single button.
|
||||
pub struct ButtonRow {
|
||||
/// The actual GTK widget.
|
||||
pub widget: libadwaita::ActionRow,
|
||||
|
||||
/// The managed button.
|
||||
button: gtk::Button,
|
||||
}
|
||||
|
||||
impl ButtonRow {
|
||||
/// Create a new button row.
|
||||
pub fn new(title: &str, label: &str) -> Self {
|
||||
let button = gtk::ButtonBuilder::new()
|
||||
.valign(gtk::Align::Center)
|
||||
.label(label)
|
||||
.build();
|
||||
|
||||
let widget = libadwaita::ActionRowBuilder::new()
|
||||
.activatable(true)
|
||||
.activatable_widget(&button)
|
||||
.title(title)
|
||||
.build();
|
||||
|
||||
widget.add_suffix(&button);
|
||||
|
||||
Self {
|
||||
widget,
|
||||
button,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the subtitle of the row.
|
||||
pub fn set_subtitle(&self, subtitle: Option<&str>) {
|
||||
self.widget.set_subtitle(subtitle);
|
||||
}
|
||||
|
||||
/// Set the closure to be called on activation
|
||||
pub fn set_cb<F: Fn() + 'static>(&self, cb: F) {
|
||||
self.button.connect_clicked(move |_| cb());
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ButtonRow {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
use super::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
|
||||
/// Common UI elements for an editor.
|
||||
pub struct Editor {
|
||||
/// The actual GTK widget.
|
||||
pub widget: gtk::Stack,
|
||||
|
||||
/// The button to switch to the previous screen.
|
||||
back_button: gtk::Button,
|
||||
|
||||
/// The title widget within the header bar.
|
||||
window_title: libadwaita::WindowTitle,
|
||||
|
||||
/// The button to save the edited item.
|
||||
save_button: gtk::Button,
|
||||
|
||||
/// The box containing the content.
|
||||
content_box: gtk::Box,
|
||||
|
||||
/// The status page for the error screen.
|
||||
status_page: libadwaita::StatusPage,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
/// Create a new screen.
|
||||
pub fn new() -> Self {
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/editor.ui");
|
||||
|
||||
get_widget!(builder, gtk::Stack, widget);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, libadwaita::WindowTitle, window_title);
|
||||
get_widget!(builder, gtk::Button, save_button);
|
||||
get_widget!(builder, gtk::Box, content_box);
|
||||
get_widget!(builder, libadwaita::StatusPage, status_page);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
|
||||
try_again_button.connect_clicked(clone!(@strong widget => move |_| {
|
||||
widget.set_visible_child_name("content");
|
||||
}));
|
||||
|
||||
Self {
|
||||
widget,
|
||||
back_button,
|
||||
window_title,
|
||||
save_button,
|
||||
content_box,
|
||||
status_page,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a closure to be called when the back button is pressed.
|
||||
pub fn set_back_cb<F: Fn() + 'static>(&self, cb: F) {
|
||||
self.back_button.connect_clicked(move |_| cb());
|
||||
}
|
||||
|
||||
/// Show a title in the header bar.
|
||||
pub fn set_title(&self, title: &str) {
|
||||
self.window_title.set_title(Some(title));
|
||||
}
|
||||
|
||||
/// Set whether the user should be able to click the save button.
|
||||
pub fn set_may_save(&self, save: bool) {
|
||||
self.save_button.set_sensitive(save);
|
||||
}
|
||||
|
||||
pub fn set_save_cb<F: Fn() + 'static>(&self, cb: F) {
|
||||
self.save_button.connect_clicked(move |_| cb());
|
||||
}
|
||||
|
||||
/// Show a loading page.
|
||||
pub fn loading(&self) {
|
||||
self.widget.set_visible_child_name("loading");
|
||||
}
|
||||
|
||||
/// Show an error page. The page contains a button to get back to the
|
||||
/// actual editor.
|
||||
pub fn error(&self, title: &str, description: &str) {
|
||||
self.status_page.set_title(Some(title));
|
||||
self.status_page.set_description(Some(description));
|
||||
self.widget.set_visible_child_name("error");
|
||||
}
|
||||
|
||||
/// Add content to the bottom of the content area.
|
||||
pub fn add_content<W: Widget>(&self, content: &W) {
|
||||
self.content_box.append(&content.get_widget());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
use gtk::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
|
||||
/// A list box row with an entry.
|
||||
pub struct EntryRow {
|
||||
/// The actual GTK widget.
|
||||
pub widget: libadwaita::ActionRow,
|
||||
|
||||
/// The managed entry.
|
||||
entry: gtk::Entry,
|
||||
}
|
||||
|
||||
impl EntryRow {
|
||||
/// Create a new entry row.
|
||||
pub fn new(title: &str) -> Self {
|
||||
let entry = gtk::EntryBuilder::new()
|
||||
.hexpand(true)
|
||||
.valign(gtk::Align::Center)
|
||||
.build();
|
||||
|
||||
let widget = libadwaita::ActionRowBuilder::new()
|
||||
.activatable(true)
|
||||
.activatable_widget(&entry)
|
||||
.title(title)
|
||||
.build();
|
||||
|
||||
widget.add_suffix(&entry);
|
||||
|
||||
Self {
|
||||
widget,
|
||||
entry,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the text of the entry.
|
||||
pub fn set_text(&self, text: &str) {
|
||||
self.entry.set_text(text);
|
||||
}
|
||||
|
||||
/// Get the text that was entered by the user.
|
||||
pub fn get_text(&self) -> String {
|
||||
self.entry.get_text().unwrap().to_string()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
use glib::prelude::*;
|
||||
use glib::subclass;
|
||||
use glib::subclass::prelude::*;
|
||||
use gio::prelude::*;
|
||||
use gio::subclass::prelude::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::cell::Cell;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct IndexedListModel(ObjectSubclass<indexed_list_model::IndexedListModel>)
|
||||
@implements gio::ListModel;
|
||||
}
|
||||
|
||||
impl IndexedListModel {
|
||||
/// Create a new indexed list model, which will be empty initially.
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[]).unwrap()
|
||||
}
|
||||
|
||||
/// Set the length of the list model.
|
||||
pub fn set_length(&self, length: u32) {
|
||||
let old_length = self.get_property("length").unwrap().get_some::<u32>().unwrap();
|
||||
self.set_property("length", &length).unwrap();
|
||||
self.items_changed(0, old_length, length);
|
||||
}
|
||||
}
|
||||
|
||||
mod indexed_list_model {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IndexedListModel {
|
||||
length: Cell<u32>,
|
||||
}
|
||||
|
||||
impl ObjectSubclass for IndexedListModel {
|
||||
const NAME: &'static str = "IndexedListModel";
|
||||
|
||||
type Type = super::IndexedListModel;
|
||||
type ParentType = glib::Object;
|
||||
type Interfaces = (gio::ListModel,);
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib::object_subclass!();
|
||||
|
||||
fn new() -> Self {
|
||||
Self { length: Cell::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for IndexedListModel {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpec::uint(
|
||||
"length",
|
||||
"Length",
|
||||
"Length",
|
||||
0,
|
||||
std::u32::MAX,
|
||||
0,
|
||||
glib::ParamFlags::READWRITE,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.get_name() {
|
||||
"length" => {
|
||||
let length = value.get().unwrap().unwrap();
|
||||
self.length.set(length);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.get_name() {
|
||||
"length" => self.length.get().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListModelImpl for IndexedListModel {
|
||||
fn get_item_type(&self, _: &Self::Type) -> glib::Type {
|
||||
ItemIndex::static_type()
|
||||
}
|
||||
|
||||
fn get_n_items(&self, _: &Self::Type) -> u32 {
|
||||
self.length.get()
|
||||
}
|
||||
|
||||
fn get_item(&self, _: &Self::Type, position: u32) -> Option<glib::Object> {
|
||||
Some(ItemIndex::new(position).upcast())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ItemIndex(ObjectSubclass<item_index::ItemIndex>);
|
||||
}
|
||||
|
||||
impl ItemIndex {
|
||||
/// Create a new item index.
|
||||
pub fn new(value: u32) -> Self {
|
||||
glib::Object::new(&[("value", &value)]).unwrap()
|
||||
}
|
||||
|
||||
/// Get the value of the item index..
|
||||
pub fn get(&self) -> u32 {
|
||||
self.get_property("value").unwrap().get_some::<u32>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
mod item_index {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ItemIndex {
|
||||
value: Cell<u32>,
|
||||
}
|
||||
|
||||
impl ObjectSubclass for ItemIndex {
|
||||
const NAME: &'static str = "ItemIndex";
|
||||
|
||||
type Type = super::ItemIndex;
|
||||
type ParentType = glib::Object;
|
||||
type Interfaces = ();
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib::object_subclass!();
|
||||
|
||||
fn new() -> Self {
|
||||
Self { value: Cell::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ItemIndex {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpec::uint(
|
||||
"value",
|
||||
"Value",
|
||||
"Value",
|
||||
0,
|
||||
std::u32::MAX,
|
||||
0,
|
||||
glib::ParamFlags::READWRITE,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.get_name() {
|
||||
"value" => {
|
||||
let value = value.get().unwrap().unwrap();
|
||||
self.value.set(value);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.get_name() {
|
||||
"value" => self.value.get().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
use super::indexed_list_model::{IndexedListModel, ItemIndex};
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A simple list of widgets.
|
||||
pub struct List {
|
||||
pub widget: gtk::ListBox,
|
||||
model: IndexedListModel,
|
||||
filter: gtk::CustomFilter,
|
||||
enable_dnd: Cell<bool>,
|
||||
make_widget_cb: RefCell<Option<Box<dyn Fn(usize) -> gtk::Widget>>>,
|
||||
filter_cb: RefCell<Option<Box<dyn Fn(usize) -> bool>>>,
|
||||
move_cb: RefCell<Option<Box<dyn Fn(usize, usize)>>>,
|
||||
}
|
||||
|
||||
impl List {
|
||||
/// Create a new list. The list will be empty initially.
|
||||
pub fn new() -> Rc<Self> {
|
||||
let model = IndexedListModel::new();
|
||||
let filter = gtk::CustomFilter::new(|_| true);
|
||||
let filter_model = gtk::FilterListModel::new(Some(&model), Some(&filter));
|
||||
|
||||
// TODO: Switch to gtk::ListView.
|
||||
// let selection = gtk::NoSelection::new(Some(&model));
|
||||
// let factory = gtk::SignalListItemFactory::new();
|
||||
// let widget = gtk::ListView::new(Some(&selection), Some(&factory));
|
||||
|
||||
let widget = gtk::ListBox::new();
|
||||
widget.set_selection_mode(gtk::SelectionMode::None);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
model,
|
||||
filter,
|
||||
enable_dnd: Cell::new(false),
|
||||
make_widget_cb: RefCell::new(None),
|
||||
filter_cb: RefCell::new(None),
|
||||
move_cb: RefCell::new(None),
|
||||
});
|
||||
|
||||
this.filter.set_filter_func(clone!(@strong this => move |index| {
|
||||
if let Some(cb) = &*this.filter_cb.borrow() {
|
||||
let index = index.downcast_ref::<ItemIndex>().unwrap().get() as usize;
|
||||
cb(index)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}));
|
||||
|
||||
this.widget.bind_model(Some(&filter_model), clone!(@strong this => move |index| {
|
||||
let index = index.downcast_ref::<ItemIndex>().unwrap().get() as usize;
|
||||
if let Some(cb) = &*this.make_widget_cb.borrow() {
|
||||
let widget = cb(index);
|
||||
|
||||
if this.enable_dnd.get() {
|
||||
let drag_source = gtk::DragSource::new();
|
||||
|
||||
drag_source.connect_drag_begin(clone!(@strong widget => move |_, drag| {
|
||||
// TODO: Replace with a better solution.
|
||||
let paintable = gtk::WidgetPaintable::new(Some(&widget));
|
||||
gtk::DragIcon::set_from_paintable(drag, &paintable, 0, 0);
|
||||
}));
|
||||
|
||||
let drag_value = (index as u32).to_value();
|
||||
drag_source.set_content(Some(&gdk::ContentProvider::new_for_value(&drag_value)));
|
||||
|
||||
let drop_target = gtk::DropTarget::new(glib::Type::U32, gdk::DragAction::COPY);
|
||||
|
||||
drop_target.connect_drop(clone!(@strong this => move |_, value, _, _| {
|
||||
if let Some(cb) = &*this.move_cb.borrow() {
|
||||
let old_index: u32 = value.get_some().unwrap();
|
||||
cb(old_index as usize, index);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}));
|
||||
|
||||
widget.add_controller(&drag_source);
|
||||
widget.add_controller(&drop_target);
|
||||
}
|
||||
|
||||
widget
|
||||
} else {
|
||||
// This shouldn't be reachable under normal circumstances.
|
||||
gtk::Label::new(None).upcast()
|
||||
}
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// Whether the list should support drag and drop.
|
||||
pub fn set_enable_dnd(&self, enable: bool) {
|
||||
self.enable_dnd.set(enable);
|
||||
}
|
||||
|
||||
/// Set the closure to be called to construct widgets for the items.
|
||||
pub fn set_make_widget_cb<F: Fn(usize) -> gtk::Widget + 'static>(&self, cb: F) {
|
||||
self.make_widget_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Set the closure to be called to filter the items. If this returns
|
||||
/// false, the item will not be shown.
|
||||
pub fn set_filter_cb<F: Fn(usize) -> bool + 'static>(&self, cb: F) {
|
||||
self.filter_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Set the closure to be called to when the use has dragged an item to a
|
||||
/// new position.
|
||||
pub fn set_move_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) {
|
||||
self.move_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Set the lists selection mode to single.
|
||||
pub fn enable_selection(&self) {
|
||||
self.widget.set_selection_mode(gtk::SelectionMode::Single);
|
||||
}
|
||||
|
||||
/// Refilter the list based on the filter callback.
|
||||
pub fn invalidate_filter(&self) {
|
||||
self.filter.changed(gtk::FilterChange::Different);
|
||||
}
|
||||
|
||||
/// Call the make_widget function for each item. This will automatically
|
||||
/// show all children by indices 0..length.
|
||||
pub fn update(&self, length: usize) {
|
||||
self.model.set_length(length as u32);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
use gtk::prelude::*;
|
||||
|
||||
pub mod button_row;
|
||||
pub use button_row::*;
|
||||
|
||||
pub mod editor;
|
||||
pub use editor::*;
|
||||
|
||||
pub mod entry_row;
|
||||
pub use entry_row::*;
|
||||
|
||||
pub mod list;
|
||||
pub use list::*;
|
||||
|
||||
pub mod player_bar;
|
||||
pub use player_bar::*;
|
||||
|
||||
pub mod screen;
|
||||
pub use screen::*;
|
||||
|
||||
pub mod section;
|
||||
pub use section::*;
|
||||
|
||||
pub mod upload_section;
|
||||
pub use upload_section::*;
|
||||
|
||||
mod indexed_list_model;
|
||||
|
||||
/// Something that can be represented as a GTK widget.
|
||||
pub trait Widget {
|
||||
/// Get the widget.
|
||||
fn get_widget(&self) -> gtk::Widget;
|
||||
}
|
||||
|
||||
impl<W: IsA<gtk::Widget>> Widget for W {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::{Player, PlaylistItem};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct PlayerBar {
|
||||
pub widget: gtk::Revealer,
|
||||
title_label: gtk::Label,
|
||||
subtitle_label: gtk::Label,
|
||||
previous_button: gtk::Button,
|
||||
play_button: gtk::Button,
|
||||
next_button: gtk::Button,
|
||||
position_label: gtk::Label,
|
||||
duration_label: gtk::Label,
|
||||
play_image: gtk::Image,
|
||||
pause_image: gtk::Image,
|
||||
player: Rc<RefCell<Option<Rc<Player>>>>,
|
||||
playlist_cb: Rc<RefCell<Option<Box<dyn Fn() -> ()>>>>,
|
||||
}
|
||||
|
||||
impl PlayerBar {
|
||||
pub fn new() -> Self {
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_bar.ui");
|
||||
|
||||
get_widget!(builder, gtk::Revealer, widget);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Label, subtitle_label);
|
||||
get_widget!(builder, gtk::Button, previous_button);
|
||||
get_widget!(builder, gtk::Button, play_button);
|
||||
get_widget!(builder, gtk::Button, next_button);
|
||||
get_widget!(builder, gtk::Label, position_label);
|
||||
get_widget!(builder, gtk::Label, duration_label);
|
||||
get_widget!(builder, gtk::Button, playlist_button);
|
||||
get_widget!(builder, gtk::Image, play_image);
|
||||
get_widget!(builder, gtk::Image, pause_image);
|
||||
|
||||
let player = Rc::new(RefCell::new(None::<Rc<Player>>));
|
||||
let playlist_cb = Rc::new(RefCell::new(None::<Box<dyn Fn() -> ()>>));
|
||||
|
||||
previous_button.connect_clicked(clone!(@strong player => move |_| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
player.previous().unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
play_button.connect_clicked(clone!(@strong player => move |_| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
player.play_pause();
|
||||
}
|
||||
}));
|
||||
|
||||
next_button.connect_clicked(clone!(@strong player => move |_| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
player.next().unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
playlist_button.connect_clicked(clone!(@strong playlist_cb => move |_| {
|
||||
if let Some(cb) = &*playlist_cb.borrow() {
|
||||
cb();
|
||||
}
|
||||
}));
|
||||
|
||||
Self {
|
||||
widget,
|
||||
title_label,
|
||||
subtitle_label,
|
||||
previous_button,
|
||||
play_button,
|
||||
next_button,
|
||||
position_label,
|
||||
duration_label,
|
||||
play_image,
|
||||
pause_image,
|
||||
player: player,
|
||||
playlist_cb: playlist_cb,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_player(&self, player: Option<Rc<Player>>) {
|
||||
self.player.replace(player.clone());
|
||||
|
||||
if let Some(player) = player {
|
||||
let playlist = Rc::new(RefCell::new(Vec::<PlaylistItem>::new()));
|
||||
|
||||
player.add_playlist_cb(clone!(
|
||||
@strong player,
|
||||
@strong self.widget as widget,
|
||||
@strong self.previous_button as previous_button,
|
||||
@strong self.next_button as next_button,
|
||||
@strong playlist
|
||||
=> move |new_playlist| {
|
||||
widget.set_reveal_child(!new_playlist.is_empty());
|
||||
playlist.replace(new_playlist);
|
||||
previous_button.set_sensitive(player.has_previous());
|
||||
next_button.set_sensitive(player.has_next());
|
||||
}
|
||||
));
|
||||
|
||||
player.add_track_cb(clone!(
|
||||
@strong player,
|
||||
@strong playlist,
|
||||
@strong self.previous_button as previous_button,
|
||||
@strong self.next_button as next_button,
|
||||
@strong self.title_label as title_label,
|
||||
@strong self.subtitle_label as subtitle_label,
|
||||
@strong self.position_label as position_label
|
||||
=> move |current_item, current_track| {
|
||||
previous_button.set_sensitive(player.has_previous());
|
||||
next_button.set_sensitive(player.has_next());
|
||||
|
||||
let item = &playlist.borrow()[current_item];
|
||||
let track = &item.track_set.tracks[current_track];
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let mut title = item.track_set.recording.work.get_title();
|
||||
if !parts.is_empty() {
|
||||
title = format!("{}: {}", title, parts.join(", "));
|
||||
}
|
||||
|
||||
title_label.set_text(&title);
|
||||
subtitle_label.set_text(&item.track_set.recording.get_performers());
|
||||
position_label.set_text("0:00");
|
||||
}
|
||||
));
|
||||
|
||||
player.add_duration_cb(clone!(
|
||||
@strong self.duration_label as duration_label
|
||||
=> move |ms| {
|
||||
let min = ms / 60000;
|
||||
let sec = (ms % 60000) / 1000;
|
||||
duration_label.set_text(&format!("{}:{:02}", min, sec));
|
||||
}
|
||||
));
|
||||
|
||||
player.add_playing_cb(clone!(
|
||||
@strong self.play_button as play_button,
|
||||
@strong self.play_image as play_image,
|
||||
@strong self.pause_image as pause_image
|
||||
=> move |playing| {
|
||||
play_button.set_child(Some(if playing {
|
||||
&pause_image
|
||||
} else {
|
||||
&play_image
|
||||
}));
|
||||
}
|
||||
));
|
||||
|
||||
player.add_position_cb(clone!(
|
||||
@strong self.position_label as position_label
|
||||
=> move |ms| {
|
||||
let min = ms / 60000;
|
||||
let sec = (ms % 60000) / 1000;
|
||||
position_label.set_text(&format!("{}:{:02}", min, sec));
|
||||
}
|
||||
));
|
||||
} else {
|
||||
self.widget.set_reveal_child(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_playlist_cb<F: Fn() -> () + 'static>(&self, cb: F) {
|
||||
self.playlist_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
use gio::prelude::*;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
|
||||
/// A general framework for screens. Screens have a header bar with at least
|
||||
/// a button to go back and a scrollable content area that clamps its content.
|
||||
pub struct Screen {
|
||||
/// The actual GTK widget.
|
||||
pub widget: gtk::Box,
|
||||
|
||||
/// The button to switch to the previous screen.
|
||||
back_button: gtk::Button,
|
||||
|
||||
/// The title widget within the header bar.
|
||||
window_title: libadwaita::WindowTitle,
|
||||
|
||||
/// The action menu.
|
||||
menu: gio::Menu,
|
||||
|
||||
/// The entry for searching.
|
||||
search_entry: gtk::SearchEntry,
|
||||
|
||||
/// The stack to switch to the loading page.
|
||||
stack: gtk::Stack,
|
||||
|
||||
/// The box containing the content.
|
||||
content_box: gtk::Box,
|
||||
|
||||
/// The actions for the menu.
|
||||
actions: gio::SimpleActionGroup,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
/// Create a new screen.
|
||||
pub fn new() -> Self {
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, libadwaita::WindowTitle, window_title);
|
||||
get_widget!(builder, gio::Menu, menu);
|
||||
get_widget!(builder, gtk::ToggleButton, search_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::Box, content_box);
|
||||
|
||||
let actions = gio::SimpleActionGroup::new();
|
||||
widget.insert_action_group("widget", Some(&actions));
|
||||
|
||||
search_button.connect_toggled(clone!(@strong search_entry => move |search_button| {
|
||||
if search_button.get_active() {
|
||||
search_entry.grab_focus();
|
||||
}
|
||||
}));
|
||||
|
||||
Self {
|
||||
widget,
|
||||
back_button,
|
||||
window_title,
|
||||
menu,
|
||||
search_entry,
|
||||
stack,
|
||||
content_box,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a closure to be called when the back button is pressed.
|
||||
pub fn set_back_cb<F: Fn() + 'static>(&self, cb: F) {
|
||||
self.back_button.connect_clicked(move |_| cb());
|
||||
}
|
||||
|
||||
/// Show a title in the header bar.
|
||||
pub fn set_title(&self, title: &str) {
|
||||
self.window_title.set_title(Some(title));
|
||||
}
|
||||
|
||||
/// Show a subtitle in the header bar.
|
||||
pub fn set_subtitle(&self, subtitle: &str) {
|
||||
self.window_title.set_subtitle(Some(subtitle));
|
||||
}
|
||||
|
||||
/// Add a new item to the action menu and register a callback for it.
|
||||
pub fn add_action<F: Fn() + 'static>(&self, label: &str, cb: F) {
|
||||
let name = rand::random::<u64>().to_string();
|
||||
let action = gio::SimpleAction::new(&name, None);
|
||||
action.connect_activate(move |_, _| cb());
|
||||
|
||||
self.actions.add_action(&action);
|
||||
self.menu.append(Some(label), Some(&format!("widget.{}", name)));
|
||||
}
|
||||
|
||||
/// Set the closure to be called when the search string has changed.
|
||||
pub fn set_search_cb<F: Fn() + 'static>(&self, cb: F) {
|
||||
self.search_entry.connect_search_changed(move |_| cb());
|
||||
}
|
||||
|
||||
/// Get the current search string.
|
||||
pub fn get_search(&self) -> String {
|
||||
self.search_entry.get_text().unwrap().to_string().to_lowercase()
|
||||
}
|
||||
|
||||
/// Hide the loading page and switch to the content.
|
||||
pub fn ready(&self) {
|
||||
self.stack.set_visible_child_name("content");
|
||||
}
|
||||
|
||||
/// Add content to the bottom of the content area.
|
||||
pub fn add_content<W: IsA<gtk::Widget>>(&self, content: &W) {
|
||||
self.content_box.append(content);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
use super::Widget;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
|
||||
/// A widget displaying a title, a framed child widget and, if needed, some
|
||||
/// actions.
|
||||
pub struct Section {
|
||||
/// The actual GTK widget.
|
||||
pub widget: gtk::Box,
|
||||
|
||||
/// The box containing the title and action buttons.
|
||||
title_box: gtk::Box,
|
||||
|
||||
/// An optional subtitle below the title.
|
||||
subtitle_label: gtk::Label,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
/// Create a new section.
|
||||
pub fn new<W: Widget>(title: &str, content: &W) -> Self {
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, gtk::Box, title_box);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Label, subtitle_label);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
|
||||
title_label.set_label(title);
|
||||
frame.set_child(Some(&content.get_widget()));
|
||||
|
||||
Self {
|
||||
widget,
|
||||
title_box,
|
||||
subtitle_label,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a subtitle below the title.
|
||||
pub fn set_subtitle(&self, subtitle: &str) {
|
||||
self.subtitle_label.set_label(subtitle);
|
||||
self.subtitle_label.show();
|
||||
}
|
||||
|
||||
/// Add an action button. This should by definition be something that is
|
||||
/// doing something with the child widget that is applicable in all
|
||||
/// situations where the widget is visible. The new button will be packed
|
||||
/// to the end of the title box.
|
||||
pub fn add_action<F: Fn() + 'static>(&self, icon_name: &str, cb: F) {
|
||||
let button = gtk::ButtonBuilder::new()
|
||||
.has_frame(false)
|
||||
.valign(gtk::Align::Center)
|
||||
.margin_top(12)
|
||||
.icon_name(icon_name)
|
||||
.build();
|
||||
|
||||
button.connect_clicked(move |_| cb());
|
||||
|
||||
self.title_box.append(&button);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Section {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use super::Section;
|
||||
|
||||
use gettextrs::gettext;
|
||||
use libadwaita::prelude::*;
|
||||
|
||||
/// A section showing a switch to enable uploading an item.
|
||||
pub struct UploadSection {
|
||||
/// The GTK widget of the wrapped section.
|
||||
pub widget: gtk::Box,
|
||||
|
||||
/// The upload switch.
|
||||
switch: gtk::Switch,
|
||||
}
|
||||
|
||||
impl UploadSection {
|
||||
/// Create a new upload section which will be initially switched on.
|
||||
pub fn new() -> Self {
|
||||
let list = gtk::ListBoxBuilder::new()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.build();
|
||||
|
||||
let switch = gtk::SwitchBuilder::new()
|
||||
.active(true)
|
||||
.valign(gtk::Align::Center)
|
||||
.build();
|
||||
|
||||
let row = libadwaita::ActionRowBuilder::new()
|
||||
.title("Upload changes to the server")
|
||||
.activatable(true)
|
||||
.activatable_widget(&switch)
|
||||
.build();
|
||||
|
||||
row.add_suffix(&switch);
|
||||
list.append(&row);
|
||||
|
||||
let section = Section::new(&gettext("Upload"), &list);
|
||||
|
||||
Self {
|
||||
widget: section.widget.clone(),
|
||||
switch,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return whether the user has enabled the upload switch.
|
||||
pub fn get_active(&self) -> bool {
|
||||
self.switch.get_active()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
use crate::screens::{MainScreen, WelcomeScreen};
|
||||
use crate::navigator::Navigator;
|
||||
use gtk::prelude::*;
|
||||
use musicus_backend::{Backend, BackendState};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// The main window of this application. This will also handle initializing and managing the
|
||||
/// backend.
|
||||
pub struct Window {
|
||||
window: libadwaita::ApplicationWindow,
|
||||
backend: Rc<Backend>,
|
||||
navigator: Rc<Navigator>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(app: >k::Application) -> Rc<Self> {
|
||||
let backend = Rc::new(Backend::new());
|
||||
|
||||
let window = libadwaita::ApplicationWindow::new(app);
|
||||
window.set_title(Some("Musicus"));
|
||||
window.set_default_size(1000, 707);
|
||||
|
||||
let loading_screen = gtk::BoxBuilder::new()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
|
||||
let header = libadwaita::HeaderBarBuilder::new()
|
||||
.title_widget(&libadwaita::WindowTitle::new(Some("Musicus"), None))
|
||||
.build();
|
||||
|
||||
let spinner = gtk::SpinnerBuilder::new()
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.halign(gtk::Align::Center)
|
||||
.valign(gtk::Align::Center)
|
||||
.width_request(32)
|
||||
.height_request(32)
|
||||
.spinning(true)
|
||||
.build();
|
||||
|
||||
loading_screen.append(&header);
|
||||
loading_screen.append(&spinner);
|
||||
|
||||
|
||||
let navigator = Navigator::new(Rc::clone(&backend), &window, &loading_screen);
|
||||
libadwaita::ApplicationWindowExt::set_child(&window, Some(&navigator.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
backend,
|
||||
window,
|
||||
navigator,
|
||||
});
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
while let Some(state) = this.backend.next_state().await {
|
||||
match state {
|
||||
BackendState::Loading => this.navigator.reset(),
|
||||
BackendState::NoMusicLibrary => this.show_welcome_screen(),
|
||||
BackendState::Ready => this.show_main_screen(),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
// This is not done in the async block above, because backend state changes may happen
|
||||
// while this method is running.
|
||||
this.backend.init().await.unwrap();
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// Present this window to the user.
|
||||
pub fn present(&self) {
|
||||
self.window.present();
|
||||
}
|
||||
|
||||
/// Replace the current screen with the welcome screen.
|
||||
fn show_welcome_screen(self: &Rc<Self>) {
|
||||
let this = self;
|
||||
spawn!(@clone this, async move {
|
||||
replace!(this.navigator, WelcomeScreen).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Replace the current screen with the main screen.
|
||||
fn show_main_screen(self: &Rc<Self>) {
|
||||
let this = self;
|
||||
spawn!(@clone this, async move {
|
||||
replace!(this.navigator, MainScreen).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
[package]
|
||||
name = "musicus_backend"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
fragile = "1.0.0"
|
||||
futures = "0.3.6"
|
||||
futures-channel = "0.3.5"
|
||||
gio = "0.9.1"
|
||||
glib = "0.10.3"
|
||||
gstreamer = "0.16.4"
|
||||
gstreamer-player = "0.16.3"
|
||||
log = "0.4.14"
|
||||
mpris-player = "0.6.0"
|
||||
musicus_client = { version = "0.1.0", path = "../musicus_client" }
|
||||
musicus_database = { version = "0.1.0", path = "../musicus_database" }
|
||||
secret-service = "2.0.1"
|
||||
thiserror = "1.0.23"
|
||||
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/// An error that can happened within the backend.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ClientError(#[from] musicus_client::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] musicus_database::Error),
|
||||
|
||||
#[error("An error happened using the SecretService.")]
|
||||
SecretServiceError(#[from] secret_service::Error),
|
||||
|
||||
#[error("A channel was canceled.")]
|
||||
ChannelError(#[from] futures_channel::oneshot::Canceled),
|
||||
|
||||
#[error("An error happened while decoding to UTF-8.")]
|
||||
Utf8Error(#[from] std::str::Utf8Error),
|
||||
|
||||
#[error("An error happened: {0}")]
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
use futures::prelude::*;
|
||||
use futures_channel::mpsc;
|
||||
use gio::prelude::*;
|
||||
use log::warn;
|
||||
use musicus_client::{Client, LoginData};
|
||||
use musicus_database::DbThread;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub use musicus_client as client;
|
||||
pub use musicus_database as db;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
pub mod library;
|
||||
pub use library::*;
|
||||
|
||||
pub mod player;
|
||||
pub use player::*;
|
||||
|
||||
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 {
|
||||
/// A future resolving to the next state of the backend. Initially, this should be assumed to
|
||||
/// be BackendState::Loading. Changes should be awaited before calling init().
|
||||
state_stream: RefCell<mpsc::Receiver<BackendState>>,
|
||||
|
||||
/// The internal sender to publish the state via state_stream.
|
||||
state_sender: RefCell<mpsc::Sender<BackendState>>,
|
||||
|
||||
/// Access to GSettings.
|
||||
settings: gio::Settings,
|
||||
|
||||
/// The current path to the music library, which is used by the player and the database. This
|
||||
/// is guaranteed to be Some, when the state is set to BackendState::Ready.
|
||||
music_library_path: RefCell<Option<PathBuf>>,
|
||||
|
||||
/// The database. This can be assumed to exist, when the state is set to BackendState::Ready.
|
||||
database: RefCell<Option<Rc<DbThread>>>,
|
||||
|
||||
/// The player handling playlist and playback. This can be assumed to exist, when the state is
|
||||
/// set to BackendState::Ready.
|
||||
player: RefCell<Option<Rc<Player>>>,
|
||||
|
||||
/// A client for the Wolfgang server.
|
||||
client: Client,
|
||||
}
|
||||
|
||||
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),
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the next state change. Initially, the state should be assumed to be
|
||||
/// BackendState::Loading. Changes should be awaited before calling init().
|
||||
pub async fn next_state(&self) -> Option<BackendState> {
|
||||
self.state_stream.borrow_mut().next().await
|
||||
}
|
||||
|
||||
/// Initialize the backend updating the state accordingly.
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
self.init_library().await?;
|
||||
|
||||
if let Some(url) = self.settings.get_string("server-url") {
|
||||
if !url.is_empty() {
|
||||
self.client.set_server_url(&url);
|
||||
}
|
||||
}
|
||||
|
||||
match Self::load_login_data().await {
|
||||
Ok(Some(data)) => self.client.set_login_data(Some(data)),
|
||||
Err(err) => warn!("The login data could not be loaded from SecretService. It will not \
|
||||
be available. Error message: {}", err),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if self.get_music_library_path().is_none() {
|
||||
self.set_state(BackendState::NoMusicLibrary);
|
||||
} else {
|
||||
self.set_state(BackendState::Ready);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the URL of the Musicus server to connect to.
|
||||
pub fn set_server_url(&self, url: &str) {
|
||||
if let Err(err) = self.settings.set_string("server-url", url) {
|
||||
warn!("An error happened while trying to save the server URL to GSettings. Most \
|
||||
likely it will not be available at the next startup. Error message: {}", err);
|
||||
}
|
||||
|
||||
self.client.set_server_url(url);
|
||||
}
|
||||
|
||||
/// Get the currently set server URL.
|
||||
pub fn get_server_url(&self) -> Option<String> {
|
||||
self.client.get_server_url()
|
||||
}
|
||||
|
||||
/// Set the user credentials to use.
|
||||
pub async fn set_login_data(&self, data: Option<LoginData>) {
|
||||
if let Some(data) = &data {
|
||||
if let Err(err) = Self::store_login_data(data.clone()).await {
|
||||
warn!("An error happened while trying to store the login data using SecretService. \
|
||||
This means, that they will not be available at the next startup most likely. \
|
||||
Error message: {}", err);
|
||||
}
|
||||
} else {
|
||||
if let Err(err) = Self::delete_secrets().await {
|
||||
warn!("An error happened while trying to delete the login data from SecretService. \
|
||||
This may result in the login data being reloaded at the next startup. Error \
|
||||
message: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
self.client.set_login_data(data);
|
||||
}
|
||||
|
||||
pub fn cl(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
/// Get the currently stored login credentials.
|
||||
pub fn get_login_data(&self) -> Option<LoginData> {
|
||||
self.client.get_login_data()
|
||||
}
|
||||
|
||||
/// Set the current state and notify the user interface.
|
||||
fn set_state(&self, state: BackendState) {
|
||||
self.state_sender.borrow_mut().try_send(state).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
use crate::{Backend, BackendState, Player, Result};
|
||||
use musicus_database::DbThread;
|
||||
use gio::prelude::*;
|
||||
use log::warn;
|
||||
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<()> {
|
||||
if let Err(err) = self.settings.set_string("music-library-path", path.to_str().unwrap()) {
|
||||
warn!("The music library path could not be saved using GSettings. It will most likely \
|
||||
not be available at the next startup. Error message: {}", err);
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,391 +0,0 @@
|
|||
use crate::{Error, Result};
|
||||
use mpris_player::{Metadata, MprisPlayer, PlaybackStatus};
|
||||
use musicus_database::TrackSet;
|
||||
use glib::clone;
|
||||
use gstreamer_player::prelude::*;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PlaylistItem {
|
||||
pub track_set: TrackSet,
|
||||
pub indices: Vec<usize>,
|
||||
}
|
||||
|
||||
pub struct Player {
|
||||
music_library_path: PathBuf,
|
||||
player: gstreamer_player::Player,
|
||||
mpris: Arc<MprisPlayer>,
|
||||
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)>>>,
|
||||
raise_cb: RefCell<Option<Box<dyn Fn()>>>,
|
||||
}
|
||||
|
||||
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 mpris = MprisPlayer::new(
|
||||
"de.johrpan.musicus".to_string(),
|
||||
"Musicus".to_string(),
|
||||
"de.johrpan.musicus.desktop".to_string(),
|
||||
);
|
||||
|
||||
|
||||
mpris.set_can_raise(true);
|
||||
mpris.set_can_play(false);
|
||||
mpris.set_can_go_previous(false);
|
||||
mpris.set_can_go_next(false);
|
||||
mpris.set_can_seek(false);
|
||||
mpris.set_can_set_fullscreen(false);
|
||||
|
||||
let result = Rc::new(Self {
|
||||
music_library_path,
|
||||
player: player.clone(),
|
||||
mpris,
|
||||
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()),
|
||||
raise_cb: RefCell::new(None),
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
clone.mpris.set_playback_status(PlaybackStatus::Paused);
|
||||
}
|
||||
});
|
||||
|
||||
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.mpris.connect_play_pause(clone!(@weak result => move || {
|
||||
result.play_pause();
|
||||
}));
|
||||
|
||||
result.mpris.connect_play(clone!(@weak result => move || {
|
||||
if !result.is_playing() {
|
||||
result.play_pause();
|
||||
}
|
||||
}));
|
||||
|
||||
result.mpris.connect_pause(clone!(@weak result => move || {
|
||||
if result.is_playing() {
|
||||
result.play_pause();
|
||||
}
|
||||
}));
|
||||
|
||||
result.mpris.connect_previous(clone!(@weak result => move || {
|
||||
let _ = result.previous();
|
||||
}));
|
||||
|
||||
result.mpris.connect_next(clone!(@weak result => move || {
|
||||
let _ = result.next();
|
||||
}));
|
||||
|
||||
result.mpris.connect_raise(clone!(@weak result => move || {
|
||||
let cb = result.raise_cb.borrow();
|
||||
if let Some(cb) = &*cb {
|
||||
cb()
|
||||
}
|
||||
}));
|
||||
|
||||
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 set_raise_cb<F: Fn() + 'static>(&self, cb: F) {
|
||||
self.raise_cb.replace(Some(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(Error::Other("Tried to add an empty playlist item!"))
|
||||
} 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);
|
||||
}
|
||||
|
||||
self.mpris.set_can_play(true);
|
||||
self.mpris.set_playback_status(PlaybackStatus::Playing);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
self.mpris.set_playback_status(PlaybackStatus::Paused);
|
||||
} else {
|
||||
self.player.play();
|
||||
self.playing.set(true);
|
||||
|
||||
for cb in &*self.playing_cbs.borrow() {
|
||||
cb(true);
|
||||
}
|
||||
|
||||
self.mpris.set_playback_status(PlaybackStatus::Playing);
|
||||
}
|
||||
}
|
||||
|
||||
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(Error::Other("Player tried to access non existant current item."))?;
|
||||
|
||||
let mut current_track = self
|
||||
.current_track
|
||||
.get()
|
||||
.ok_or(Error::Other("Player tried to access non existant 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(Error::Other("No existing 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(Error::Other("Player tried to access non existant current item."))?;
|
||||
let mut current_track = self
|
||||
.current_track
|
||||
.get()
|
||||
.ok_or(Error::Other("Player tried to access non existant 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(Error::Other("No existing previous track."));
|
||||
}
|
||||
|
||||
self.set_track(current_item, current_track)
|
||||
}
|
||||
|
||||
pub fn set_track(&self, current_item: usize, current_track: usize) -> Result<()> {
|
||||
let item = &self.playlist.borrow()[current_item];
|
||||
let track = &item.track_set.tracks[current_track];
|
||||
|
||||
let uri = format!(
|
||||
"file://{}",
|
||||
self.music_library_path.join(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);
|
||||
}
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let mut title = item.track_set.recording.work.get_title();
|
||||
if !parts.is_empty() {
|
||||
title = format!("{}: {}", title, parts.join(", "));
|
||||
}
|
||||
|
||||
let subtitle = item.track_set.recording.get_performers();
|
||||
|
||||
let mut metadata = Metadata::new();
|
||||
metadata.artist = Some(vec![title]);
|
||||
metadata.title = Some(subtitle);
|
||||
|
||||
self.mpris.set_metadata(metadata);
|
||||
self.mpris.set_can_go_previous(self.has_previous());
|
||||
self.mpris.set_can_go_next(self.has_next());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_data(&self) {
|
||||
for cb in &*self.playlist_cbs.borrow() {
|
||||
cb(self.playlist.borrow().clone());
|
||||
}
|
||||
|
||||
for cb in &*self.track_cbs.borrow() {
|
||||
cb(self.current_item.get().unwrap(), self.current_track.get().unwrap());
|
||||
}
|
||||
|
||||
for cb in &*self.duration_cbs.borrow() {
|
||||
cb(self.player.get_duration().mseconds().unwrap());
|
||||
}
|
||||
|
||||
for cb in &*self.playing_cbs.borrow() {
|
||||
cb(self.is_playing());
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
self.mpris.set_can_play(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
use crate::{Backend, Error, Result};
|
||||
use musicus_client::LoginData;
|
||||
use futures_channel::oneshot;
|
||||
use secret_service::{Collection, EncryptionType, SecretService};
|
||||
use std::collections::HashMap;
|
||||
use std::thread;
|
||||
|
||||
impl Backend {
|
||||
/// Get the login credentials from secret storage.
|
||||
pub(super) async fn load_login_data() -> Result<Option<LoginData>> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::load_login_data_priv()).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
pub(super) async fn store_login_data(data: LoginData) -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::store_login_data_priv(data)).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Delete all stored secrets.
|
||||
pub(super) async fn delete_secrets() -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::delete_secrets_priv()).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Get the login credentials from secret storage.
|
||||
fn load_login_data_priv() -> Result<Option<LoginData>> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let items = collection.get_all_items()?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
let item = items.iter().find(|item| item.get_label().unwrap_or_default() == key);
|
||||
|
||||
Ok(match item {
|
||||
Some(item) => {
|
||||
let username = item
|
||||
.get_attributes()?
|
||||
.get("username")
|
||||
.ok_or(Error::Other("Missing username in SecretService attributes."))?
|
||||
.to_owned();
|
||||
|
||||
let password = std::str::from_utf8(&item.get_secret()?)?.to_owned();
|
||||
|
||||
Some(LoginData { username, password })
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
fn store_login_data_priv(data: LoginData) -> Result<()> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
Self::delete_secrets_for_key(&collection, key)?;
|
||||
|
||||
let mut attributes = HashMap::new();
|
||||
attributes.insert("username", data.username.as_str());
|
||||
collection.create_item(key, attributes, data.password.as_bytes(), true, "text/plain")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all stored secrets.
|
||||
fn delete_secrets_priv() -> Result<()> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
Self::delete_secrets_for_key(&collection, key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all stored secrets for the provided key.
|
||||
fn delete_secrets_for_key(collection: &Collection, key: &str) -> Result<()> {
|
||||
let items = collection.get_all_items()?;
|
||||
|
||||
for item in items {
|
||||
if item.get_label().unwrap_or_default() == key {
|
||||
item.delete()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the default SecretService collection and unlock it.
|
||||
fn get_collection<'a>(ss: &'a SecretService) -> Result<Collection<'a>> {
|
||||
let collection = ss.get_default_collection()?;
|
||||
collection.unlock()?;
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
[package]
|
||||
name = "musicus_client"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
isahc = "1.1.0"
|
||||
musicus_database = { version = "0.1.0", path = "../musicus_database" }
|
||||
serde = { version = "1.0.117", features = ["derive"] }
|
||||
serde_json = "1.0.59"
|
||||
thiserror = "1.0.23"
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use musicus_database::Ensemble;
|
||||
|
||||
impl Client {
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue