mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Merge branch 'wip/cd-ripping'
This commit is contained in:
commit
2b9cff885b
37 changed files with 2214 additions and 1136 deletions
|
|
@ -7,6 +7,7 @@ edition = "2018"
|
||||||
anyhow = "1.0.33"
|
anyhow = "1.0.33"
|
||||||
diesel = { version = "1.4.5", features = ["sqlite"] }
|
diesel = { version = "1.4.5", features = ["sqlite"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
|
discid = "0.4.4"
|
||||||
fragile = "1.0.0"
|
fragile = "1.0.0"
|
||||||
futures = "0.3.6"
|
futures = "0.3.6"
|
||||||
futures-channel = "0.3.5"
|
futures-channel = "0.3.5"
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
DROP TABLE persons;
|
PRAGMA defer_foreign_keys;
|
||||||
|
|
||||||
DROP TABLE instruments;
|
DROP TABLE "persons";
|
||||||
|
DROP TABLE "instruments";
|
||||||
|
DROP TABLE "works";
|
||||||
|
DROP TABLE "instrumentations";
|
||||||
|
DROP TABLE "work_parts";
|
||||||
|
DROP TABLE "work_sections";
|
||||||
|
DROP TABLE "ensembles";
|
||||||
|
DROP TABLE "recordings";
|
||||||
|
DROP TABLE "performances";
|
||||||
|
DROP TABLE "mediums";
|
||||||
|
DROP TABLE "track_sets";
|
||||||
|
DROP TABLE "tracks";
|
||||||
|
|
||||||
DROP TABLE works;
|
|
||||||
|
|
||||||
DROP TABLE instrumentations;
|
|
||||||
|
|
||||||
DROP TABLE work_parts;
|
|
||||||
|
|
||||||
DROP TABLE work_sections;
|
|
||||||
|
|
||||||
DROP TABLE ensembles;
|
|
||||||
|
|
||||||
DROP TABLE recordings;
|
|
||||||
|
|
||||||
DROP TABLE performances;
|
|
||||||
|
|
||||||
DROP TABLE tracks;
|
|
||||||
|
|
@ -1,64 +1,78 @@
|
||||||
CREATE TABLE persons (
|
CREATE TABLE "persons" (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
first_name TEXT NOT NULL,
|
"first_name" TEXT NOT NULL,
|
||||||
last_name TEXT NOT NULL
|
"last_name" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE instruments (
|
CREATE TABLE "instruments" (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
name TEXT NOT NULL
|
"name" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE works (
|
CREATE TABLE "works" (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
composer TEXT NOT NULL REFERENCES persons(id),
|
"composer" TEXT NOT NULL REFERENCES "persons"("id"),
|
||||||
title TEXT NOT NULL
|
"title" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE instrumentations (
|
CREATE TABLE "instrumentations" (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
"id" BIGINT NOT NULL PRIMARY KEY,
|
||||||
work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
"work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
|
||||||
instrument TEXT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE
|
"instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE work_parts (
|
CREATE TABLE "work_parts" (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
"id" BIGINT NOT NULL PRIMARY KEY,
|
||||||
work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
"work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
|
||||||
part_index BIGINT NOT NULL,
|
"part_index" BIGINT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
"title" TEXT NOT NULL,
|
||||||
composer TEXT REFERENCES persons(id)
|
"composer" TEXT REFERENCES "persons"("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE work_sections (
|
CREATE TABLE "work_sections" (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
"id" BIGINT NOT NULL PRIMARY KEY,
|
||||||
work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
"work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
|
||||||
title TEXT NOT NULL,
|
"title" TEXT NOT NULL,
|
||||||
before_index BIGINT NOT NULL
|
"before_index" BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ensembles (
|
CREATE TABLE "ensembles" (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
name TEXT NOT NULL
|
"name" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE recordings (
|
CREATE TABLE "recordings" (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
work TEXT NOT NULL REFERENCES works(id),
|
"work" TEXT NOT NULL REFERENCES "works"("id"),
|
||||||
comment TEXT NOT NULL
|
"comment" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE performances (
|
CREATE TABLE "performances" (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
"id" BIGINT NOT NULL PRIMARY KEY,
|
||||||
recording TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
|
"recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE,
|
||||||
person TEXT REFERENCES persons(id),
|
"person" TEXT REFERENCES "persons"("id"),
|
||||||
ensemble TEXT REFERENCES ensembles(id),
|
"ensemble" TEXT REFERENCES "ensembles"("id"),
|
||||||
role TEXT REFERENCES instruments(id)
|
"role" TEXT REFERENCES "instruments"("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE tracks (
|
CREATE TABLE "mediums" (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
file_name TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
recording TEXT NOT NULL REFERENCES recordings(id),
|
"discid" TEXT
|
||||||
track_index INTEGER NOT NULL,
|
|
||||||
work_parts TEXT NOT NULL
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "track_sets" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE,
|
||||||
|
"index" INTEGER NOT NULL,
|
||||||
|
"recording" TEXT NOT NULL REFERENCES "recordings"("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "tracks" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"track_set" TEXT NOT NULL REFERENCES "track_sets"("id") ON DELETE CASCADE,
|
||||||
|
"index" INTEGER NOT NULL,
|
||||||
|
"work_parts" TEXT NOT NULL,
|
||||||
|
"path" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/de/johrpan/musicus">
|
<gresource prefix="/de/johrpan/musicus">
|
||||||
<file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/ensemble_selector.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
|
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/ensemble_selector.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/instrument_selector.ui</file>
|
<file preprocess="xml-stripblanks">ui/instrument_selector.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
|
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/medium_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/performance_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/performance_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/person_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/person_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/person_list.ui</file>
|
<file preprocess="xml-stripblanks">ui/person_list.ui</file>
|
||||||
|
|
@ -22,8 +23,10 @@
|
||||||
<file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file>
|
<file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/selector.ui</file>
|
<file preprocess="xml-stripblanks">ui/selector.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/server_dialog.ui</file>
|
<file preprocess="xml-stripblanks">ui/server_dialog.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/tracks_editor.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_editor.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/track_selector.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/track_set_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/work_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/work_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/work_part_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/work_part_editor.ui</file>
|
||||||
|
|
|
||||||
201
musicus/res/ui/medium_editor.ui
Normal file
201
musicus/res/ui/medium_editor.ui
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.24"/>
|
||||||
|
<requires lib="libhandy" version="1.0"/>
|
||||||
|
<object class="GtkStack" id="widget">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="transition-type">crossfade</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyHeaderBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="title" translatable="yes">Import music</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="back_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">go-previous-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="done_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="sensitive">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkStack" id="done_stack">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="transition-type">crossfade</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSpinner" id="spinner">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="active">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage" id="done">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">object-select-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<style>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="pack-type">end</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkInfoBar" id="info_bar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="revealed">False</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyClamp">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<property name="label" translatable="yes">Medium</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="shadow-type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="selection-mode">none</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyActionRow" id="name_row">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="activatable">True</property>
|
||||||
|
<property name="title" translatable="yes">Name of the medium</property>
|
||||||
|
<property name="activatable-widget">name_entry</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="name_entry">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="HdyActionRow">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="activatable">True</property>
|
||||||
|
<property name="title" translatable="yes">Publish to the server</property>
|
||||||
|
<property name="activatable-widget">publish_switch</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSwitch" id="publish_switch">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="active">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="valign">end</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label" translatable="yes">Recordings</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="add_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="relief">none</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">list-add-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame" id="frame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="shadow-type">in</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">content</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSpinner">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="active">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">loading</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
161
musicus/res/ui/source_selector.ui
Normal file
161
musicus/res/ui/source_selector.ui
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.24"/>
|
||||||
|
<requires lib="libhandy" version="1.0"/>
|
||||||
|
<object class="GtkBox" id="widget">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyHeaderBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="title" translatable="yes">Import music</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="back_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="icon-name">go-previous-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkStack" id="stack">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="transition-type">crossfade</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkInfoBar" id="info_bar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="message-type">error</property>
|
||||||
|
<property name="revealed">False</property>
|
||||||
|
<child internal-child="content_area">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="spacing">16</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes">Failed to load the CD. Make sure you have inserted it into your drive.</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="border-width">18</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">18</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="opacity">0.5019607843137255</property>
|
||||||
|
<property name="pixel-size">80</property>
|
||||||
|
<property name="icon-name">media-optical-cd-audio-symbolic</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="opacity">0.5019607843137255</property>
|
||||||
|
<property name="label" translatable="yes">Import from audio CD</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="size" value="16384"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="opacity">0.5019607843137255</property>
|
||||||
|
<property name="label" translatable="yes">Insert an audio compact disc into your drive and click the button below. The disc will be copied in the background while you set up the metadata.</property>
|
||||||
|
<property name="justify">center</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="max-width-chars">40</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="import_button">
|
||||||
|
<property name="label" translatable="yes">Import</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<style>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">start</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSpinner">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="active">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">loading</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
|
|
@ -1,107 +1,67 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Generated with glade 3.38.1 -->
|
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.24"/>
|
<requires lib="gtk+" version="3.24"/>
|
||||||
<requires lib="libhandy" version="0.0"/>
|
<requires lib="libhandy" version="1.0"/>
|
||||||
<object class="GtkBox" id="widget">
|
<object class="GtkBox" id="widget">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="HdyHeaderBar">
|
<object class="HdyHeaderBar">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="title" translatable="yes">Track</property>
|
<property name="title" translatable="yes">Track</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="back_button">
|
<object class="GtkButton" id="back_button">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage">
|
<object class="GtkImage">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">go-previous-symbolic</property>
|
<property name="icon-name">go-previous-symbolic</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="save_button">
|
<object class="GtkButton" id="select_button">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">object-select-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<style>
|
<style>
|
||||||
<class name="suggested-action"/>
|
<class name="suggested-action"/>
|
||||||
</style>
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">object-select-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="pack-type">end</property>
|
<property name="pack-type">end</property>
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow">
|
<object class="GtkScrolledWindow">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkViewport">
|
<object class="GtkViewport">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="shadow-type">none</property>
|
<property name="shadow-type">none</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="HdyClamp">
|
<object class="HdyClamp">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="maximum-size">500</property>
|
|
||||||
<property name="tightening-threshold">300</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkFrame">
|
<object class="GtkFrame" id="parts_frame">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="valign">start</property>
|
<property name="valign">start</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
<property name="margin-start">6</property>
|
<property name="margin-start">6</property>
|
||||||
<property name="margin-end">6</property>
|
<property name="margin-end">6</property>
|
||||||
<property name="margin-top">12</property>
|
|
||||||
<property name="margin-bottom">6</property>
|
<property name="margin-bottom">6</property>
|
||||||
<property name="label-xalign">0</property>
|
|
||||||
<property name="shadow-type">in</property>
|
<property name="shadow-type">in</property>
|
||||||
<child>
|
|
||||||
<object class="GtkListBox" id="list">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="selection-mode">none</property>
|
|
||||||
<child type="placeholder">
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="margin-start">6</property>
|
|
||||||
<property name="margin-end">6</property>
|
|
||||||
<property name="margin-top">6</property>
|
|
||||||
<property name="margin-bottom">6</property>
|
|
||||||
<property name="label" translatable="yes">Select a recording of a work with multiple parts.</property>
|
|
||||||
<property name="ellipsize">end</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child type="label_item">
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|
@ -109,11 +69,6 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
|
||||||
75
musicus/res/ui/track_selector.ui
Normal file
75
musicus/res/ui/track_selector.ui
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.24"/>
|
||||||
|
<requires lib="libhandy" version="1.0"/>
|
||||||
|
<object class="GtkBox" id="widget">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyHeaderBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="title" translatable="yes">Select tracks</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="back_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">go-previous-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="select_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="sensitive">False</property>
|
||||||
|
<style>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">object-select-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="pack-type">end</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="shadow-type">none</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyClamp">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame" id="tracks_frame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="valign">start</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<property name="shadow-type">in</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
153
musicus/res/ui/track_set_editor.ui
Normal file
153
musicus/res/ui/track_set_editor.ui
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.24"/>
|
||||||
|
<requires lib="libhandy" version="1.0"/>
|
||||||
|
<object class="GtkBox" id="widget">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyHeaderBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="title" translatable="yes">Tracks</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="back_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">go-previous-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="save_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="sensitive">False</property>
|
||||||
|
<style>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">object-select-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="pack-type">end</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="shadow-type">none</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyClamp">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<property name="label" translatable="yes">Recording</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="shadow-type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="selection-mode">none</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyActionRow" id="recording_row">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="activatable">True</property>
|
||||||
|
<property name="title" translatable="yes">Select a recording</property>
|
||||||
|
<property name="activatable-widget">select_recording_button</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="select_recording_button">
|
||||||
|
<property name="label" translatable="yes">Select</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="valign">end</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label" translatable="yes">Tracks</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="edit_tracks_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="relief">none</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">document-edit-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame" id="tracks_frame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="shadow-type">in</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
|
|
@ -1,311 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!-- Generated with glade 3.38.1 -->
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="3.24"/>
|
|
||||||
<requires lib="libhandy" version="0.0"/>
|
|
||||||
<object class="GtkBox" id="widget">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="HdyHeaderBar">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="title" translatable="yes">Tracks</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="save_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">object-select-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<style>
|
|
||||||
<class name="suggested-action"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="pack-type">end</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="back_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">go-previous-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="HdyClamp">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="vexpand">True</property>
|
|
||||||
<property name="maximum-size">800</property>
|
|
||||||
<property name="tightening-threshold">300</property>
|
|
||||||
<child>
|
|
||||||
<!-- n-columns=2 n-rows=2 -->
|
|
||||||
<object class="GtkGrid">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="vexpand">True</property>
|
|
||||||
<property name="border-width">18</property>
|
|
||||||
<property name="row-spacing">12</property>
|
|
||||||
<property name="column-spacing">6</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">end</property>
|
|
||||||
<property name="label" translatable="yes">Recording</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">0</property>
|
|
||||||
<property name="top-attach">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="recording_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<property name="hexpand">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkStack" id="recording_stack">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="vhomogeneous">False</property>
|
|
||||||
<property name="transition-type">crossfade</property>
|
|
||||||
<property name="interpolate-size">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label" translatable="yes">Select …</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="name">unselected</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="work_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label" translatable="yes">Work</property>
|
|
||||||
<property name="ellipsize">end</property>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="weight" value="bold"/>
|
|
||||||
</attributes>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="performers_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="opacity">0.5</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="label" translatable="yes">Performers</property>
|
|
||||||
<property name="ellipsize">end</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="name">selected</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">1</property>
|
|
||||||
<property name="top-attach">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="vexpand">True</property>
|
|
||||||
<property name="spacing">6</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow" id="scroll">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="shadow-type">in</property>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">6</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="add_track_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">list-add-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="edit_track_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">edit-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="remove_track_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">list-remove-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="move_track_down_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">go-down-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="pack-type">end</property>
|
|
||||||
<property name="position">4</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="move_track_up_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">True</property>
|
|
||||||
<property name="receives-default">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can-focus">False</property>
|
|
||||||
<property name="icon-name">go-up-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="pack-type">end</property>
|
|
||||||
<property name="position">5</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left-attach">0</property>
|
|
||||||
<property name="top-attach">1</property>
|
|
||||||
<property name="width">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||
|
|
@ -327,6 +327,10 @@
|
||||||
</object>
|
</object>
|
||||||
<menu id="menu">
|
<menu id="menu">
|
||||||
<section>
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">Import CD</attribute>
|
||||||
|
<attribute name="action">win.import-disc</attribute>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<attribute name="label" translatable="yes">Preferences</attribute>
|
<attribute name="label" translatable="yes">Preferences</attribute>
|
||||||
<attribute name="action">win.preferences</attribute>
|
<attribute name="action">win.preferences</attribute>
|
||||||
|
|
|
||||||
254
musicus/src/database/medium.rs
Normal file
254
musicus/src/database/medium.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
use super::generate_id;
|
||||||
|
use super::schema::{mediums, recordings, track_sets, tracks};
|
||||||
|
use super::{Database, Recording};
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Representation of someting like a physical audio disc or a folder with
|
||||||
|
/// audio files (i.e. a collection of tracks for one or more recordings).
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Medium {
|
||||||
|
/// An unique ID for the medium.
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
/// The human identifier for the medium.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// If applicable, the MusicBrainz DiscID.
|
||||||
|
pub discid: Option<String>,
|
||||||
|
|
||||||
|
/// The tracks of the medium, grouped by recording.
|
||||||
|
pub tracks: Vec<TrackSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of tracks of one recording within a medium.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TrackSet {
|
||||||
|
/// The recording to which the tracks belong.
|
||||||
|
pub recording: Recording,
|
||||||
|
|
||||||
|
/// The actual tracks.
|
||||||
|
pub tracks: Vec<Track>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A track within a recording on a medium.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Track {
|
||||||
|
/// The work parts that are played on this track. They are indices to the
|
||||||
|
/// work parts of the work that is associated with the recording.
|
||||||
|
pub work_parts: Vec<usize>,
|
||||||
|
|
||||||
|
/// The path to the audio file containing this track. This will not be
|
||||||
|
/// included when communicating with the server.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Table data for a [`Medium`].
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
#[table_name = "mediums"]
|
||||||
|
struct MediumRow {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub discid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Table data for a [`TrackSet`].
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
#[table_name = "track_sets"]
|
||||||
|
struct TrackSetRow {
|
||||||
|
pub id: String,
|
||||||
|
pub medium: String,
|
||||||
|
pub index: i32,
|
||||||
|
pub recording: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Table data for a [`Track`].
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
#[table_name = "tracks"]
|
||||||
|
struct TrackRow {
|
||||||
|
pub id: String,
|
||||||
|
pub track_set: String,
|
||||||
|
pub index: i32,
|
||||||
|
pub work_parts: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
/// Update an existing medium or insert a new one.
|
||||||
|
pub fn update_medium(&self, medium: Medium) -> Result<()> {
|
||||||
|
self.defer_foreign_keys()?;
|
||||||
|
|
||||||
|
self.connection.transaction::<(), Error, _>(|| {
|
||||||
|
let medium_id = &medium.id;
|
||||||
|
|
||||||
|
// This will also delete the track sets and tracks.
|
||||||
|
self.delete_medium(medium_id)?;
|
||||||
|
|
||||||
|
// Add the new medium.
|
||||||
|
|
||||||
|
let medium_row = MediumRow {
|
||||||
|
id: medium_id.to_owned(),
|
||||||
|
name: medium.name.clone(),
|
||||||
|
discid: medium.discid.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(mediums::table)
|
||||||
|
.values(medium_row)
|
||||||
|
.execute(&self.connection)?;
|
||||||
|
|
||||||
|
for (index, track_set) in medium.tracks.iter().enumerate() {
|
||||||
|
// Add associated items from the server, if they don't already
|
||||||
|
// exist.
|
||||||
|
|
||||||
|
if self.get_recording(&track_set.recording.id)?.is_none() {
|
||||||
|
self.update_recording(track_set.recording.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the actual track set data.
|
||||||
|
|
||||||
|
let track_set_id = generate_id();
|
||||||
|
|
||||||
|
let track_set_row = TrackSetRow {
|
||||||
|
id: track_set_id.clone(),
|
||||||
|
medium: medium_id.to_owned(),
|
||||||
|
index: index as i32,
|
||||||
|
recording: track_set.recording.id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(track_sets::table)
|
||||||
|
.values(track_set_row)
|
||||||
|
.execute(&self.connection)?;
|
||||||
|
|
||||||
|
for (index, track) in track_set.tracks.iter().enumerate() {
|
||||||
|
let work_parts = track
|
||||||
|
.work_parts
|
||||||
|
.iter()
|
||||||
|
.map(|part_index| part_index.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
let track_row = TrackRow {
|
||||||
|
id: generate_id(),
|
||||||
|
track_set: track_set_id.clone(),
|
||||||
|
index: index as i32,
|
||||||
|
work_parts,
|
||||||
|
path: track.path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(tracks::table)
|
||||||
|
.values(track_row)
|
||||||
|
.execute(&self.connection)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing medium.
|
||||||
|
pub fn get_medium(&self, id: &str) -> Result<Option<Medium>> {
|
||||||
|
let row = mediums::table
|
||||||
|
.filter(mediums::id.eq(id))
|
||||||
|
.load::<MediumRow>(&self.connection)?
|
||||||
|
.into_iter()
|
||||||
|
.next();
|
||||||
|
|
||||||
|
let medium = match row {
|
||||||
|
Some(row) => Some(self.get_medium_data(row)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a medium and all of its tracks. This will fail, if the music
|
||||||
|
/// library contains audio files referencing any of those tracks.
|
||||||
|
pub fn delete_medium(&self, id: &str) -> Result<()> {
|
||||||
|
diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(&self.connection)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all available track sets for a recording.
|
||||||
|
pub fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
|
||||||
|
let mut track_sets: Vec<TrackSet> = Vec::new();
|
||||||
|
|
||||||
|
let rows = track_sets::table
|
||||||
|
.inner_join(recordings::table.on(recordings::id.eq(track_sets::recording)))
|
||||||
|
.filter(recordings::id.eq(recording_id))
|
||||||
|
.select(track_sets::table::all_columns())
|
||||||
|
.load::<TrackSetRow>(&self.connection)?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let track_set = self.get_track_set_from_row(row)?;
|
||||||
|
track_sets.push(track_set);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(track_sets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve all available information on a medium from related tables.
|
||||||
|
fn get_medium_data(&self, row: MediumRow) -> Result<Medium> {
|
||||||
|
let track_set_rows = track_sets::table
|
||||||
|
.filter(track_sets::medium.eq(&row.id))
|
||||||
|
.order_by(track_sets::index)
|
||||||
|
.load::<TrackSetRow>(&self.connection)?;
|
||||||
|
|
||||||
|
let mut track_sets = Vec::new();
|
||||||
|
|
||||||
|
for track_set_row in track_set_rows {
|
||||||
|
let track_set = self.get_track_set_from_row(track_set_row)?;
|
||||||
|
track_sets.push(track_set);
|
||||||
|
}
|
||||||
|
|
||||||
|
let medium = Medium {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
discid: row.discid,
|
||||||
|
tracks: track_sets,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a track set row from the database to an actual track set.
|
||||||
|
fn get_track_set_from_row(&self, row: TrackSetRow) -> Result<TrackSet> {
|
||||||
|
let recording_id = row.recording;
|
||||||
|
|
||||||
|
let recording = self
|
||||||
|
.get_recording(&recording_id)?
|
||||||
|
.ok_or_else(|| anyhow!("No recording with ID: {}", recording_id))?;
|
||||||
|
|
||||||
|
let track_rows = tracks::table
|
||||||
|
.filter(tracks::track_set.eq(row.id))
|
||||||
|
.order_by(tracks::index)
|
||||||
|
.load::<TrackRow>(&self.connection)?;
|
||||||
|
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
|
||||||
|
for track_row in track_rows {
|
||||||
|
let work_parts = track_row
|
||||||
|
.work_parts
|
||||||
|
.split(',')
|
||||||
|
.map(|part_index| Ok(str::parse(part_index)?))
|
||||||
|
.collect::<Result<Vec<usize>>>()?;
|
||||||
|
|
||||||
|
let track = Track {
|
||||||
|
work_parts,
|
||||||
|
path: track_row.path,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracks.push(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
let track_set = TrackSet { recording, tracks };
|
||||||
|
|
||||||
|
Ok(track_set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,9 @@ pub use ensembles::*;
|
||||||
pub mod instruments;
|
pub mod instruments;
|
||||||
pub use instruments::*;
|
pub use instruments::*;
|
||||||
|
|
||||||
|
pub mod medium;
|
||||||
|
pub use medium::*;
|
||||||
|
|
||||||
pub mod persons;
|
pub mod persons;
|
||||||
pub use persons::*;
|
pub use persons::*;
|
||||||
|
|
||||||
|
|
@ -16,9 +19,6 @@ pub use recordings::*;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
pub use thread::*;
|
pub use thread::*;
|
||||||
|
|
||||||
pub mod tracks;
|
|
||||||
pub use tracks::*;
|
|
||||||
|
|
||||||
pub mod works;
|
pub mod works;
|
||||||
pub use works::*;
|
pub use works::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,22 @@ impl Database {
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an existing recording.
|
||||||
|
pub fn get_recording(&self, id: &str) -> Result<Option<Recording>> {
|
||||||
|
let row = recordings::table
|
||||||
|
.filter(recordings::id.eq(id))
|
||||||
|
.load::<RecordingRow>(&self.connection)?
|
||||||
|
.into_iter()
|
||||||
|
.next();
|
||||||
|
|
||||||
|
let recording = match row {
|
||||||
|
Some(row) => Some(self.get_recording_data(row)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(recording)
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve all available information on a recording from related tables.
|
/// Retrieve all available information on a recording from related tables.
|
||||||
fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> {
|
fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> {
|
||||||
let mut performance_descriptions: Vec<Performance> = Vec::new();
|
let mut performance_descriptions: Vec<Performance> = Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mediums (id) {
|
||||||
|
id -> Text,
|
||||||
|
name -> Text,
|
||||||
|
discid -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
performances (id) {
|
performances (id) {
|
||||||
id -> BigInt,
|
id -> BigInt,
|
||||||
|
|
@ -47,12 +55,21 @@ table! {
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
tracks (id) {
|
track_sets (id) {
|
||||||
id -> BigInt,
|
id -> Text,
|
||||||
file_name -> Text,
|
medium -> Text,
|
||||||
|
index -> Integer,
|
||||||
recording -> Text,
|
recording -> Text,
|
||||||
track_index -> Integer,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
tracks (id) {
|
||||||
|
id -> Text,
|
||||||
|
track_set -> Text,
|
||||||
|
index -> Integer,
|
||||||
work_parts -> Text,
|
work_parts -> Text,
|
||||||
|
path -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +107,9 @@ joinable!(performances -> instruments (role));
|
||||||
joinable!(performances -> persons (person));
|
joinable!(performances -> persons (person));
|
||||||
joinable!(performances -> recordings (recording));
|
joinable!(performances -> recordings (recording));
|
||||||
joinable!(recordings -> works (work));
|
joinable!(recordings -> works (work));
|
||||||
joinable!(tracks -> recordings (recording));
|
joinable!(track_sets -> mediums (medium));
|
||||||
|
joinable!(track_sets -> recordings (recording));
|
||||||
|
joinable!(tracks -> track_sets (track_set));
|
||||||
joinable!(work_parts -> persons (composer));
|
joinable!(work_parts -> persons (composer));
|
||||||
joinable!(work_parts -> works (work));
|
joinable!(work_parts -> works (work));
|
||||||
joinable!(work_sections -> works (work));
|
joinable!(work_sections -> works (work));
|
||||||
|
|
@ -100,9 +119,11 @@ allow_tables_to_appear_in_same_query!(
|
||||||
ensembles,
|
ensembles,
|
||||||
instrumentations,
|
instrumentations,
|
||||||
instruments,
|
instruments,
|
||||||
|
mediums,
|
||||||
performances,
|
performances,
|
||||||
persons,
|
persons,
|
||||||
recordings,
|
recordings,
|
||||||
|
track_sets,
|
||||||
tracks,
|
tracks,
|
||||||
work_parts,
|
work_parts,
|
||||||
work_sections,
|
work_sections,
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,10 @@ enum Action {
|
||||||
GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>),
|
GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>),
|
||||||
GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>),
|
GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>),
|
||||||
RecordingExists(String, Sender<Result<bool>>),
|
RecordingExists(String, Sender<Result<bool>>),
|
||||||
UpdateTracks(String, Vec<Track>, Sender<Result<()>>),
|
UpdateMedium(Medium, Sender<Result<()>>),
|
||||||
DeleteTracks(String, Sender<Result<()>>),
|
GetMedium(String, Sender<Result<Option<Medium>>>),
|
||||||
GetTracks(String, Sender<Result<Vec<Track>>>),
|
DeleteMedium(String, Sender<Result<()>>),
|
||||||
|
GetTrackSets(String, Sender<Result<Vec<TrackSet>>>),
|
||||||
Stop(Sender<()>),
|
Stop(Sender<()>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,16 +125,17 @@ impl DbThread {
|
||||||
RecordingExists(id, sender) => {
|
RecordingExists(id, sender) => {
|
||||||
sender.send(db.recording_exists(&id)).unwrap();
|
sender.send(db.recording_exists(&id)).unwrap();
|
||||||
}
|
}
|
||||||
UpdateTracks(recording_id, tracks, sender) => {
|
UpdateMedium(medium, sender) => {
|
||||||
sender
|
sender.send(db.update_medium(medium)).unwrap();
|
||||||
.send(db.update_tracks(&recording_id, tracks))
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
DeleteTracks(recording_id, sender) => {
|
GetMedium(id, sender) => {
|
||||||
sender.send(db.delete_tracks(&recording_id)).unwrap();
|
sender.send(db.get_medium(&id)).unwrap();
|
||||||
}
|
}
|
||||||
GetTracks(recording_id, sender) => {
|
DeleteMedium(id, sender) => {
|
||||||
sender.send(db.get_tracks(&recording_id)).unwrap();
|
sender.send(db.delete_medium(&id)).unwrap();
|
||||||
|
}
|
||||||
|
GetTrackSets(recording_id, sender) => {
|
||||||
|
sender.send(db.get_track_sets(&recording_id)).unwrap();
|
||||||
}
|
}
|
||||||
Stop(sender) => {
|
Stop(sender) => {
|
||||||
sender.send(()).unwrap();
|
sender.send(()).unwrap();
|
||||||
|
|
@ -312,28 +314,35 @@ impl DbThread {
|
||||||
receiver.await?
|
receiver.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add or change the tracks associated with a recording. This will fail, if there are still
|
/// Update an existing medium or insert a new one.
|
||||||
/// other items referencing this recording.
|
pub async fn update_medium(&self, medium: Medium) -> Result<()> {
|
||||||
pub async fn update_tracks(&self, recording_id: &str, tracks: Vec<Track>) -> Result<()> {
|
|
||||||
let (sender, receiver) = oneshot::channel();
|
let (sender, receiver) = oneshot::channel();
|
||||||
self.action_sender
|
self.action_sender.send(UpdateMedium(medium, sender))?;
|
||||||
.send(UpdateTracks(recording_id.to_string(), tracks, sender))?;
|
|
||||||
receiver.await?
|
receiver.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete all tracks associated with a recording.
|
/// Delete an existing medium. This will fail, if there are still other
|
||||||
pub async fn delete_tracks(&self, recording_id: &str) -> Result<()> {
|
/// items referencing this medium.
|
||||||
|
pub async fn delete_medium(&self, id: &str) -> Result<()> {
|
||||||
let (sender, receiver) = oneshot::channel();
|
let (sender, receiver) = oneshot::channel();
|
||||||
|
|
||||||
self.action_sender
|
self.action_sender
|
||||||
.send(DeleteTracks(recording_id.to_string(), sender))?;
|
.send(DeleteMedium(id.to_owned(), sender))?;
|
||||||
|
|
||||||
receiver.await?
|
receiver.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all tracks associated with a recording.
|
/// Get an existing medium.
|
||||||
pub async fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> {
|
pub async fn get_medium(&self, id: &str) -> Result<Option<Medium>> {
|
||||||
let (sender, receiver) = oneshot::channel();
|
let (sender, receiver) = oneshot::channel();
|
||||||
self.action_sender
|
self.action_sender.send(GetMedium(id.to_owned(), sender))?;
|
||||||
.send(GetTracks(recording_id.to_string(), sender))?;
|
receiver.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all track sets for a recording.
|
||||||
|
pub async fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
|
||||||
|
let (sender, receiver) = oneshot::channel();
|
||||||
|
self.action_sender.send(GetTrackSets(recording_id.to_owned(), sender))?;
|
||||||
receiver.await?
|
receiver.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
use super::schema::tracks;
|
|
||||||
use super::Database;
|
|
||||||
use anyhow::{Error, Result};
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
|
||||||
|
|
||||||
/// Table row data for a track.
|
|
||||||
#[derive(Insertable, Queryable, Debug, Clone)]
|
|
||||||
#[table_name = "tracks"]
|
|
||||||
struct TrackRow {
|
|
||||||
pub id: i64,
|
|
||||||
pub file_name: String,
|
|
||||||
pub recording: String,
|
|
||||||
pub track_index: i32,
|
|
||||||
pub work_parts: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A structure representing one playable audio file.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Track {
|
|
||||||
pub work_parts: Vec<usize>,
|
|
||||||
pub file_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<TrackRow> for Track {
|
|
||||||
type Error = Error;
|
|
||||||
fn try_from(row: TrackRow) -> Result<Self> {
|
|
||||||
let mut work_parts = Vec::<usize>::new();
|
|
||||||
for part in row.work_parts.split(",") {
|
|
||||||
if !part.is_empty() {
|
|
||||||
work_parts.push(part.parse()?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let track = Track {
|
|
||||||
work_parts,
|
|
||||||
file_name: row.file_name,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
/// Insert or update tracks for the specified recording.
|
|
||||||
pub fn update_tracks(&self, recording_id: &str, tracks: Vec<Track>) -> Result<()> {
|
|
||||||
self.delete_tracks(recording_id)?;
|
|
||||||
|
|
||||||
for (index, track) in tracks.iter().enumerate() {
|
|
||||||
let row = TrackRow {
|
|
||||||
id: rand::random(),
|
|
||||||
file_name: track.file_name.clone(),
|
|
||||||
recording: recording_id.to_string(),
|
|
||||||
track_index: index.try_into()?,
|
|
||||||
work_parts: track
|
|
||||||
.work_parts
|
|
||||||
.iter()
|
|
||||||
.map(|i| i.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(","),
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(tracks::table)
|
|
||||||
.values(row)
|
|
||||||
.execute(&self.connection)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete all tracks for the specified recording.
|
|
||||||
pub fn delete_tracks(&self, recording_id: &str) -> Result<()> {
|
|
||||||
diesel::delete(tracks::table.filter(tracks::recording.eq(recording_id)))
|
|
||||||
.execute(&self.connection)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all tracks of the specified recording.
|
|
||||||
pub fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> {
|
|
||||||
let mut tracks = Vec::<Track>::new();
|
|
||||||
|
|
||||||
let rows = tracks::table
|
|
||||||
.filter(tracks::recording.eq(recording_id))
|
|
||||||
.order_by(tracks::track_index)
|
|
||||||
.load::<TrackRow>(&self.connection)?;
|
|
||||||
|
|
||||||
for row in rows {
|
|
||||||
tracks.push(row.try_into()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tracks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,13 +10,9 @@ pub use person::*;
|
||||||
pub mod recording;
|
pub mod recording;
|
||||||
pub use recording::*;
|
pub use recording::*;
|
||||||
|
|
||||||
pub mod tracks;
|
|
||||||
pub use tracks::*;
|
|
||||||
|
|
||||||
pub mod work;
|
pub mod work;
|
||||||
pub use work::*;
|
pub use work::*;
|
||||||
|
|
||||||
mod performance;
|
mod performance;
|
||||||
mod track;
|
|
||||||
mod work_part;
|
mod work_part;
|
||||||
mod work_section;
|
mod work_section;
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
use crate::database::*;
|
|
||||||
use crate::widgets::{Navigator, NavigatorScreen};
|
|
||||||
use glib::clone;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk_macros::get_widget;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// A screen for editing a single track.
|
|
||||||
// TODO: Refactor.
|
|
||||||
pub struct TrackEditor {
|
|
||||||
widget: gtk::Box,
|
|
||||||
ready_cb: RefCell<Option<Box<dyn Fn(Track) -> ()>>>,
|
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrackEditor {
|
|
||||||
/// Create a new track editor.
|
|
||||||
pub fn new(track: Track, work: Work) -> 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, save_button);
|
|
||||||
get_widget!(builder, gtk::ListBox, list);
|
|
||||||
|
|
||||||
let this = Rc::new(Self {
|
|
||||||
widget,
|
|
||||||
ready_cb: RefCell::new(None),
|
|
||||||
navigator: RefCell::new(None),
|
|
||||||
});
|
|
||||||
|
|
||||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
let work = Rc::new(work);
|
|
||||||
let work_parts = Rc::new(RefCell::new(track.work_parts));
|
|
||||||
let file_name = track.file_name;
|
|
||||||
|
|
||||||
save_button.connect_clicked(clone!(@strong this, @strong work_parts => move |_| {
|
|
||||||
let mut work_parts = work_parts.borrow_mut();
|
|
||||||
work_parts.sort();
|
|
||||||
|
|
||||||
if let Some(cb) = &*this.ready_cb.borrow() {
|
|
||||||
cb(Track {
|
|
||||||
work_parts: work_parts.clone(),
|
|
||||||
file_name: file_name.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (index, part) in work.parts.iter().enumerate() {
|
|
||||||
let check = gtk::CheckButton::new();
|
|
||||||
check.set_active(work_parts.borrow().contains(&index));
|
|
||||||
check.connect_toggled(clone!(@strong check, @strong work_parts => move |_| {
|
|
||||||
if check.get_active() {
|
|
||||||
let mut work_parts = work_parts.borrow_mut();
|
|
||||||
work_parts.push(index);
|
|
||||||
} else {
|
|
||||||
let mut work_parts = work_parts.borrow_mut();
|
|
||||||
if let Some(pos) = work_parts.iter().position(|part| *part == index) {
|
|
||||||
work_parts.remove(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
let label = gtk::Label::new(Some(&part.title));
|
|
||||||
label.set_halign(gtk::Align::Start);
|
|
||||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
|
|
||||||
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
|
||||||
hbox.set_border_width(6);
|
|
||||||
hbox.add(&check);
|
|
||||||
hbox.add(&label);
|
|
||||||
|
|
||||||
let row = gtk::ListBoxRow::new();
|
|
||||||
row.add(&hbox);
|
|
||||||
row.show_all();
|
|
||||||
|
|
||||||
list.add(&row);
|
|
||||||
list.connect_row_activated(
|
|
||||||
clone!(@strong row, @strong check => move |_, activated_row| {
|
|
||||||
if *activated_row == row {
|
|
||||||
check.activate();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut section_count = 0;
|
|
||||||
for section in &work.sections {
|
|
||||||
let attributes = pango::AttrList::new();
|
|
||||||
attributes.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap());
|
|
||||||
|
|
||||||
let label = gtk::Label::new(Some(§ion.title));
|
|
||||||
label.set_halign(gtk::Align::Start);
|
|
||||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
label.set_attributes(Some(&attributes));
|
|
||||||
let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
||||||
wrap.set_border_width(6);
|
|
||||||
wrap.add(&label);
|
|
||||||
|
|
||||||
let row = gtk::ListBoxRow::new();
|
|
||||||
row.set_activatable(false);
|
|
||||||
row.add(&wrap);
|
|
||||||
row.show_all();
|
|
||||||
|
|
||||||
list.insert(
|
|
||||||
&row,
|
|
||||||
(section.before_index + section_count).try_into().unwrap(),
|
|
||||||
);
|
|
||||||
section_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the closure to be called when the track was edited.
|
|
||||||
pub fn set_ready_cb<F: Fn(Track) -> () + 'static>(&self, cb: F) {
|
|
||||||
self.ready_cb.replace(Some(Box::new(cb)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NavigatorScreen for TrackEditor {
|
|
||||||
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
|
||||||
self.navigator.replace(Some(navigator));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_widget(&self) -> gtk::Widget {
|
|
||||||
self.widget.clone().upcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detach_navigator(&self) {
|
|
||||||
self.navigator.replace(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,337 +0,0 @@
|
||||||
use super::track::TrackEditor;
|
|
||||||
use crate::backend::Backend;
|
|
||||||
use crate::database::*;
|
|
||||||
use crate::widgets::{List, Navigator, NavigatorScreen};
|
|
||||||
use crate::selectors::{PersonSelector, WorkSelector, RecordingSelector};
|
|
||||||
use gettextrs::gettext;
|
|
||||||
use glib::clone;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk_macros::get_widget;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// A dialog for editing a set of tracks.
|
|
||||||
// TODO: Disable buttons if no track is selected.
|
|
||||||
pub struct TracksEditor {
|
|
||||||
backend: Rc<Backend>,
|
|
||||||
widget: gtk::Box,
|
|
||||||
save_button: gtk::Button,
|
|
||||||
recording_stack: gtk::Stack,
|
|
||||||
work_label: gtk::Label,
|
|
||||||
performers_label: gtk::Label,
|
|
||||||
track_list: Rc<List<Track>>,
|
|
||||||
recording: RefCell<Option<Recording>>,
|
|
||||||
tracks: RefCell<Vec<Track>>,
|
|
||||||
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TracksEditor {
|
|
||||||
/// Create a new track editor an optionally initialize it with a recording and a list of
|
|
||||||
/// tracks.
|
|
||||||
pub fn new(
|
|
||||||
backend: Rc<Backend>,
|
|
||||||
recording: Option<Recording>,
|
|
||||||
tracks: Vec<Track>,
|
|
||||||
) -> Rc<Self> {
|
|
||||||
// UI setup
|
|
||||||
|
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui");
|
|
||||||
|
|
||||||
get_widget!(builder, gtk::Box, widget);
|
|
||||||
get_widget!(builder, gtk::Button, back_button);
|
|
||||||
get_widget!(builder, gtk::Button, save_button);
|
|
||||||
get_widget!(builder, gtk::Button, recording_button);
|
|
||||||
get_widget!(builder, gtk::Stack, recording_stack);
|
|
||||||
get_widget!(builder, gtk::Label, work_label);
|
|
||||||
get_widget!(builder, gtk::Label, performers_label);
|
|
||||||
get_widget!(builder, gtk::ScrolledWindow, scroll);
|
|
||||||
get_widget!(builder, gtk::Button, add_track_button);
|
|
||||||
get_widget!(builder, gtk::Button, edit_track_button);
|
|
||||||
get_widget!(builder, gtk::Button, remove_track_button);
|
|
||||||
get_widget!(builder, gtk::Button, move_track_up_button);
|
|
||||||
get_widget!(builder, gtk::Button, move_track_down_button);
|
|
||||||
|
|
||||||
let track_list = List::new(&gettext("Add some tracks."));
|
|
||||||
scroll.add(&track_list.widget);
|
|
||||||
|
|
||||||
let this = Rc::new(Self {
|
|
||||||
backend,
|
|
||||||
widget,
|
|
||||||
save_button,
|
|
||||||
recording_stack,
|
|
||||||
work_label,
|
|
||||||
performers_label,
|
|
||||||
track_list,
|
|
||||||
recording: RefCell::new(recording),
|
|
||||||
tracks: RefCell::new(tracks),
|
|
||||||
callback: RefCell::new(None),
|
|
||||||
navigator: RefCell::new(None),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Signals and callbacks
|
|
||||||
|
|
||||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.save_button
|
|
||||||
.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let context = glib::MainContext::default();
|
|
||||||
let this = this.clone();
|
|
||||||
context.spawn_local(async move {
|
|
||||||
let recording = this.recording.borrow().as_ref().unwrap().clone();
|
|
||||||
|
|
||||||
// Add the recording first, if it's from the server.
|
|
||||||
|
|
||||||
if !this.backend.db().recording_exists(&recording.id).await.unwrap() {
|
|
||||||
this.backend.db().update_recording(recording.clone()).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the actual tracks.
|
|
||||||
|
|
||||||
this.backend.db().update_tracks(
|
|
||||||
&recording.id,
|
|
||||||
this.tracks.borrow().clone(),
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(callback) = &*this.callback.borrow() {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
recording_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
let person_selector = PersonSelector::new(this.backend.clone());
|
|
||||||
|
|
||||||
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
|
|
||||||
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
|
|
||||||
|
|
||||||
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
|
|
||||||
let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone());
|
|
||||||
|
|
||||||
recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| {
|
|
||||||
this.recording_selected(recording);
|
|
||||||
this.recording.replace(Some(recording.clone()));
|
|
||||||
|
|
||||||
navigator.clone().pop();
|
|
||||||
navigator.clone().pop();
|
|
||||||
navigator.clone().pop();
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.clone().push(recording_selector);
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.clone().push(work_selector);
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.clone().push(person_selector);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.track_list
|
|
||||||
.set_make_widget(clone!(@strong this => move |track| {
|
|
||||||
this.build_track_row(track)
|
|
||||||
}));
|
|
||||||
|
|
||||||
add_track_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
let music_library_path = this.backend.get_music_library_path().unwrap();
|
|
||||||
|
|
||||||
let dialog = gtk::FileChooserNative::new(
|
|
||||||
Some(&gettext("Select audio files")),
|
|
||||||
Some(&navigator.window),
|
|
||||||
gtk::FileChooserAction::Open,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
dialog.set_select_multiple(true);
|
|
||||||
dialog.set_current_folder(&music_library_path);
|
|
||||||
|
|
||||||
if let gtk::ResponseType::Accept = dialog.run() {
|
|
||||||
let mut index = match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => index + 1,
|
|
||||||
None => this.tracks.borrow().len(),
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
for file_name in dialog.get_filenames() {
|
|
||||||
let file_name = file_name.strip_prefix(&music_library_path).unwrap();
|
|
||||||
tracks.insert(index, Track {
|
|
||||||
work_parts: Vec::new(),
|
|
||||||
file_name: String::from(file_name.to_str().unwrap()),
|
|
||||||
});
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.track_list.show_items(this.tracks.borrow().clone());
|
|
||||||
this.autofill_parts();
|
|
||||||
this.track_list.select_index(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
remove_track_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
tracks.remove(index);
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index);
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
move_track_up_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => {
|
|
||||||
if index > 0 {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
tracks.swap(index - 1, index);
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
move_track_down_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
if index < tracks.len() - 1 {
|
|
||||||
tracks.swap(index, index + 1);
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
edit_track_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
if let Some(index) = this.track_list.get_selected_index() {
|
|
||||||
if let Some(recording) = &*this.recording.borrow() {
|
|
||||||
let editor = TrackEditor::new(this.tracks.borrow()[index].clone(), recording.work.clone());
|
|
||||||
|
|
||||||
editor.set_ready_cb(clone!(@strong this => move |track| {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
tracks[index] = track;
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index);
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.push(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Initialization
|
|
||||||
|
|
||||||
if let Some(recording) = &*this.recording.borrow() {
|
|
||||||
this.recording_selected(recording);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.track_list.show_items(this.tracks.borrow().clone());
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a callback to be called when the tracks are saved.
|
|
||||||
pub fn set_callback<F: Fn() -> () + 'static>(&self, cb: F) {
|
|
||||||
self.callback.replace(Some(Box::new(cb)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a widget representing a track.
|
|
||||||
fn build_track_row(&self, track: &Track) -> gtk::Widget {
|
|
||||||
let mut title_parts = Vec::<String>::new();
|
|
||||||
for part in &track.work_parts {
|
|
||||||
if let Some(recording) = &*self.recording.borrow() {
|
|
||||||
title_parts.push(recording.work.parts[*part].title.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = if title_parts.is_empty() {
|
|
||||||
gettext("Unknown")
|
|
||||||
} else {
|
|
||||||
title_parts.join(", ")
|
|
||||||
};
|
|
||||||
|
|
||||||
let title_label = gtk::Label::new(Some(&title));
|
|
||||||
title_label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
title_label.set_halign(gtk::Align::Start);
|
|
||||||
|
|
||||||
let file_name_label = gtk::Label::new(Some(&track.file_name));
|
|
||||||
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
file_name_label.set_opacity(0.5);
|
|
||||||
file_name_label.set_halign(gtk::Align::Start);
|
|
||||||
|
|
||||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
||||||
vbox.set_border_width(6);
|
|
||||||
vbox.add(&title_label);
|
|
||||||
vbox.add(&file_name_label);
|
|
||||||
|
|
||||||
vbox.upcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set everything up after selecting a recording.
|
|
||||||
fn recording_selected(&self, recording: &Recording) {
|
|
||||||
self.work_label.set_text(&recording.work.get_title());
|
|
||||||
self.performers_label.set_text(&recording.get_performers());
|
|
||||||
self.recording_stack.set_visible_child_name("selected");
|
|
||||||
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.track_list.show_items(tracks.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NavigatorScreen for TracksEditor {
|
|
||||||
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
|
||||||
self.navigator.replace(Some(navigator));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_widget(&self) -> gtk::Widget {
|
|
||||||
self.widget.clone().upcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detach_navigator(&self) {
|
|
||||||
self.navigator.replace(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
175
musicus/src/import/disc_source.rs
Normal file
175
musicus/src/import/disc_source.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use discid::DiscId;
|
||||||
|
use futures_channel::oneshot;
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
use gstreamer::{Element, ElementFactory, Pipeline};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
/// Representation of an audio CD being imported as a medium.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DiscSource {
|
||||||
|
/// The MusicBrainz DiscID of the CD.
|
||||||
|
pub discid: String,
|
||||||
|
|
||||||
|
/// The path to the temporary directory where the audio files will be.
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// The tracks on this disc.
|
||||||
|
pub tracks: Vec<TrackSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Representation of a single track on an audio CD.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TrackSource {
|
||||||
|
/// The track number. This is different from the index in the disc
|
||||||
|
/// source's tracks list, because it is not defined from which number the
|
||||||
|
/// the track numbers start.
|
||||||
|
pub number: u32,
|
||||||
|
|
||||||
|
/// The path to the temporary file to which the track will be ripped. The
|
||||||
|
/// file will not exist until the track is actually ripped.
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscSource {
|
||||||
|
/// Try to create a new disc source by asynchronously reading the
|
||||||
|
/// information from the default disc drive.
|
||||||
|
pub async fn load() -> Result<Self> {
|
||||||
|
let (sender, receiver) = oneshot::channel();
|
||||||
|
|
||||||
|
thread::spawn(|| {
|
||||||
|
let disc = Self::load_priv();
|
||||||
|
sender.send(disc).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let disc = receiver.await??;
|
||||||
|
|
||||||
|
Ok(disc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rip the whole disc asynchronously. After this method has finished
|
||||||
|
/// successfully, the audio files will be available in the specified
|
||||||
|
/// location for each track source.
|
||||||
|
pub async fn rip(&self) -> Result<()> {
|
||||||
|
for track in &self.tracks {
|
||||||
|
let (sender, receiver) = oneshot::channel();
|
||||||
|
|
||||||
|
let number = track.number;
|
||||||
|
let path = track.path.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let result = Self::rip_track(&path, number);
|
||||||
|
sender.send(result).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
receiver.await??;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the disc from the default disc drive.
|
||||||
|
fn load_priv() -> Result<Self> {
|
||||||
|
let discid = DiscId::read(None)?;
|
||||||
|
let id = discid.id();
|
||||||
|
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
|
||||||
|
let first_track = discid.first_track_num() as u32;
|
||||||
|
let last_track = discid.last_track_num() as u32;
|
||||||
|
|
||||||
|
let tmp_dir = Self::create_tmp_dir()?;
|
||||||
|
|
||||||
|
for number in first_track..=last_track {
|
||||||
|
let file_name = format!("track_{:02}.flac", number);
|
||||||
|
|
||||||
|
let mut path = tmp_dir.clone();
|
||||||
|
path.push(file_name);
|
||||||
|
|
||||||
|
let track = TrackSource {
|
||||||
|
number,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracks.push(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
let disc = DiscSource {
|
||||||
|
discid: id,
|
||||||
|
tracks,
|
||||||
|
path: tmp_dir,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(disc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new temporary directory and return its path.
|
||||||
|
// TODO: Move to a more appropriate place.
|
||||||
|
fn create_tmp_dir() -> Result<PathBuf> {
|
||||||
|
let mut tmp_dir = glib::get_tmp_dir()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow!("Failed to get temporary directory using glib::get_tmp_dir()!")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let dir_name = format!("musicus-{}", rand::random::<u64>());
|
||||||
|
tmp_dir.push(dir_name);
|
||||||
|
|
||||||
|
std::fs::create_dir(&tmp_dir)?;
|
||||||
|
|
||||||
|
Ok(tmp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rip one track.
|
||||||
|
fn rip_track(path: &Path, number: u32) -> Result<()> {
|
||||||
|
let pipeline = Self::build_pipeline(path, number)?;
|
||||||
|
|
||||||
|
let bus = pipeline
|
||||||
|
.get_bus()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to get bus from pipeline!"))?;
|
||||||
|
|
||||||
|
pipeline.set_state(gstreamer::State::Playing)?;
|
||||||
|
|
||||||
|
for msg in bus.iter_timed(gstreamer::CLOCK_TIME_NONE) {
|
||||||
|
use gstreamer::MessageView::*;
|
||||||
|
|
||||||
|
match msg.view() {
|
||||||
|
Eos(..) => break,
|
||||||
|
Error(err) => {
|
||||||
|
pipeline.set_state(gstreamer::State::Null)?;
|
||||||
|
bail!("GStreamer error: {:?}!", err);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline.set_state(gstreamer::State::Null)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the GStreamer pipeline to rip a track.
|
||||||
|
fn build_pipeline(path: &Path, number: u32) -> Result<Pipeline> {
|
||||||
|
let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?;
|
||||||
|
cdparanoiasrc.set_property("track", &number)?;
|
||||||
|
|
||||||
|
let queue = ElementFactory::make("queue", None)?;
|
||||||
|
let audioconvert = ElementFactory::make("audioconvert", None)?;
|
||||||
|
let flacenc = ElementFactory::make("flacenc", None)?;
|
||||||
|
|
||||||
|
let path_str = path.to_str().ok_or_else(|| {
|
||||||
|
anyhow!("Failed to convert path '{:?}' to string!", path)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let filesink = gstreamer::ElementFactory::make("filesink", None)?;
|
||||||
|
filesink.set_property("location", &path_str.to_owned())?;
|
||||||
|
|
||||||
|
let pipeline = gstreamer::Pipeline::new(None);
|
||||||
|
pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
|
||||||
|
|
||||||
|
Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
|
||||||
|
|
||||||
|
Ok(pipeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
241
musicus/src/import/medium_editor.rs
Normal file
241
musicus/src/import/medium_editor.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
use super::disc_source::DiscSource;
|
||||||
|
use super::track_set_editor::{TrackSetData, TrackSetEditor};
|
||||||
|
use crate::database::{generate_id, Medium, Track, TrackSet};
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
|
use crate::widgets::new_list::List;
|
||||||
|
use anyhow::Result;
|
||||||
|
use glib::clone;
|
||||||
|
use glib::prelude::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use libhandy::prelude::*;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A dialog for editing metadata while importing music into the music library.
|
||||||
|
pub struct MediumEditor {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
source: Rc<DiscSource>,
|
||||||
|
widget: gtk::Stack,
|
||||||
|
done_button: gtk::Button,
|
||||||
|
done_stack: gtk::Stack,
|
||||||
|
done: gtk::Image,
|
||||||
|
name_entry: gtk::Entry,
|
||||||
|
publish_switch: gtk::Switch,
|
||||||
|
track_set_list: List,
|
||||||
|
track_sets: RefCell<Vec<TrackSetData>>,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediumEditor {
|
||||||
|
/// Create a new medium editor.
|
||||||
|
pub fn new(backend: Rc<Backend>, source: DiscSource) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Stack, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, done_button);
|
||||||
|
get_widget!(builder, gtk::Stack, done_stack);
|
||||||
|
get_widget!(builder, gtk::Image, done);
|
||||||
|
get_widget!(builder, gtk::Entry, name_entry);
|
||||||
|
get_widget!(builder, gtk::Switch, publish_switch);
|
||||||
|
get_widget!(builder, gtk::Button, add_button);
|
||||||
|
get_widget!(builder, gtk::Frame, frame);
|
||||||
|
|
||||||
|
let list = List::new("No recordings added.");
|
||||||
|
frame.add(&list.widget);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
source: Rc::new(source),
|
||||||
|
widget,
|
||||||
|
done_button,
|
||||||
|
done_stack,
|
||||||
|
done,
|
||||||
|
name_entry,
|
||||||
|
publish_switch,
|
||||||
|
track_set_list: list,
|
||||||
|
track_sets: RefCell::new(Vec::new()),
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.done_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let context = glib::MainContext::default();
|
||||||
|
let clone = this.clone();
|
||||||
|
context.spawn_local(async move {
|
||||||
|
clone.widget.set_visible_child_name("loading");
|
||||||
|
match clone.clone().save().await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(err) => {
|
||||||
|
println!("{:?}", err);
|
||||||
|
// clone.info_bar.set_revealed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
add_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let editor = TrackSetEditor::new(this.backend.clone(), Rc::clone(&this.source));
|
||||||
|
|
||||||
|
editor.set_done_cb(clone!(@strong this => move |track_set| {
|
||||||
|
let length = {
|
||||||
|
let mut track_sets = this.track_sets.borrow_mut();
|
||||||
|
track_sets.push(track_set);
|
||||||
|
track_sets.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.track_set_list.update(length);
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.push(editor);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.track_set_list.set_make_widget(clone!(@strong this => move |index| {
|
||||||
|
let track_set = &this.track_sets.borrow()[index];
|
||||||
|
|
||||||
|
let title = track_set.recording.work.get_title();
|
||||||
|
let subtitle = track_set.recording.get_performers();
|
||||||
|
|
||||||
|
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
|
||||||
|
let edit_button = gtk::Button::new();
|
||||||
|
edit_button.set_relief(gtk::ReliefStyle::None);
|
||||||
|
edit_button.set_valign(gtk::Align::Center);
|
||||||
|
edit_button.add(&edit_image);
|
||||||
|
|
||||||
|
let row = libhandy::ActionRow::new();
|
||||||
|
row.set_activatable(true);
|
||||||
|
row.set_title(Some(&title));
|
||||||
|
row.set_subtitle(Some(&subtitle));
|
||||||
|
row.add(&edit_button);
|
||||||
|
row.set_activatable_widget(Some(&edit_button));
|
||||||
|
row.show_all();
|
||||||
|
|
||||||
|
edit_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
row.upcast()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Start ripping the CD in the background.
|
||||||
|
let context = glib::MainContext::default();
|
||||||
|
let clone = this.clone();
|
||||||
|
context.spawn_local(async move {
|
||||||
|
match clone.source.rip().await {
|
||||||
|
Err(error) => {
|
||||||
|
// TODO: Present error.
|
||||||
|
println!("Failed to rip: {}", error);
|
||||||
|
},
|
||||||
|
Ok(_) => {
|
||||||
|
clone.done_stack.set_visible_child(&clone.done);
|
||||||
|
clone.done_button.set_sensitive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the medium and possibly upload it to the server.
|
||||||
|
async fn save(self: Rc<Self>) -> Result<()> {
|
||||||
|
let name = self.name_entry.get_text().to_string();
|
||||||
|
|
||||||
|
// Create a new directory in the music library path for the imported medium.
|
||||||
|
|
||||||
|
let mut path = self.backend.get_music_library_path().unwrap().clone();
|
||||||
|
path.push(&name);
|
||||||
|
std::fs::create_dir(&path)?;
|
||||||
|
|
||||||
|
// Convert the track set data to real track sets.
|
||||||
|
|
||||||
|
let mut track_sets = Vec::new();
|
||||||
|
|
||||||
|
for track_set_data in &*self.track_sets.borrow() {
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
|
||||||
|
for track_data in &track_set_data.tracks {
|
||||||
|
// Copy the corresponding audio file to the music library.
|
||||||
|
|
||||||
|
let track_source = &self.source.tracks[track_data.track_source];
|
||||||
|
let file_name = format!("track_{:02}.flac", track_source.number);
|
||||||
|
|
||||||
|
let mut track_path = path.clone();
|
||||||
|
track_path.push(&file_name);
|
||||||
|
|
||||||
|
std::fs::copy(&track_source.path, &track_path)?;
|
||||||
|
|
||||||
|
// Create the real track.
|
||||||
|
|
||||||
|
let track = Track {
|
||||||
|
work_parts: track_data.work_parts.clone(),
|
||||||
|
path: track_path.to_str().unwrap().to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
tracks.push(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
let track_set = TrackSet {
|
||||||
|
recording: track_set_data.recording.clone(),
|
||||||
|
tracks,
|
||||||
|
};
|
||||||
|
|
||||||
|
track_sets.push(track_set);
|
||||||
|
}
|
||||||
|
|
||||||
|
let medium = Medium {
|
||||||
|
id: generate_id(),
|
||||||
|
name: self.name_entry.get_text().to_string(),
|
||||||
|
discid: Some(self.source.discid.clone()),
|
||||||
|
tracks: track_sets,
|
||||||
|
};
|
||||||
|
|
||||||
|
let upload = self.publish_switch.get_active();
|
||||||
|
if upload {
|
||||||
|
// self.backend.post_medium(&medium).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backend
|
||||||
|
.db()
|
||||||
|
.update_medium(medium.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.backend.library_changed();
|
||||||
|
|
||||||
|
let navigator = self.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.clone().pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for MediumEditor {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
musicus/src/import/mod.rs
Normal file
8
musicus/src/import/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
mod disc_source;
|
||||||
|
mod medium_editor;
|
||||||
|
mod source_selector;
|
||||||
|
mod track_editor;
|
||||||
|
mod track_selector;
|
||||||
|
mod track_set_editor;
|
||||||
|
|
||||||
|
pub use source_selector::SourceSelector;
|
||||||
92
musicus/src/import/source_selector.rs
Normal file
92
musicus/src/import/source_selector.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
use super::medium_editor::MediumEditor;
|
||||||
|
use super::disc_source::DiscSource;
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
|
use anyhow::Result;
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A dialog for starting to import music.
|
||||||
|
pub struct SourceSelector {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
widget: gtk::Box,
|
||||||
|
stack: gtk::Stack,
|
||||||
|
info_bar: gtk::InfoBar,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceSelector {
|
||||||
|
/// Create a new source selector.
|
||||||
|
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Stack, stack);
|
||||||
|
get_widget!(builder, gtk::InfoBar, info_bar);
|
||||||
|
get_widget!(builder, gtk::Button, import_button);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
widget,
|
||||||
|
stack,
|
||||||
|
info_bar,
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
this.stack.set_visible_child_name("loading");
|
||||||
|
|
||||||
|
let context = glib::MainContext::default();
|
||||||
|
let clone = this.clone();
|
||||||
|
context.spawn_local(async move {
|
||||||
|
match DiscSource::load().await {
|
||||||
|
Ok(disc) => {
|
||||||
|
let navigator = clone.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let editor = MediumEditor::new(clone.backend.clone(), disc);
|
||||||
|
navigator.push(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.info_bar.set_revealed(false);
|
||||||
|
clone.stack.set_visible_child_name("start");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
clone.info_bar.set_revealed(true);
|
||||||
|
clone.stack.set_visible_child_name("start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for SourceSelector {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
musicus/src/import/track_editor.rs
Normal file
110
musicus/src/import/track_editor.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use crate::database::Recording;
|
||||||
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
|
use crate::widgets::new_list::List;
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use libhandy::prelude::*;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A screen for editing a single track.
|
||||||
|
pub struct TrackEditor {
|
||||||
|
widget: gtk::Box,
|
||||||
|
selection: RefCell<Vec<usize>>,
|
||||||
|
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackEditor {
|
||||||
|
/// Create a new track editor.
|
||||||
|
pub fn new(recording: Recording, selection: Vec<usize>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, select_button);
|
||||||
|
get_widget!(builder, gtk::Frame, parts_frame);
|
||||||
|
|
||||||
|
let parts_list = gtk::ListBox::new();
|
||||||
|
parts_list.set_selection_mode(gtk::SelectionMode::None);
|
||||||
|
parts_list.set_vexpand(false);
|
||||||
|
parts_list.show();
|
||||||
|
parts_frame.add(&parts_list);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
widget,
|
||||||
|
selection: RefCell::new(selection),
|
||||||
|
selected_cb: RefCell::new(None),
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
select_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||||
|
let selection = this.selection.borrow().clone();
|
||||||
|
cb(selection);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (index, part) in recording.work.parts.iter().enumerate() {
|
||||||
|
let check = gtk::CheckButton::new();
|
||||||
|
check.set_active(this.selection.borrow().contains(&index));
|
||||||
|
|
||||||
|
check.connect_toggled(clone!(@strong this => move |check| {
|
||||||
|
let mut selection = this.selection.borrow_mut();
|
||||||
|
if check.get_active() {
|
||||||
|
selection.push(index);
|
||||||
|
} else {
|
||||||
|
if let Some(pos) = selection.iter().position(|part| *part == index) {
|
||||||
|
selection.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let row = libhandy::ActionRow::new();
|
||||||
|
row.add_prefix(&check);
|
||||||
|
row.set_activatable_widget(Some(&check));
|
||||||
|
row.set_title(Some(&part.title));
|
||||||
|
row.show_all();
|
||||||
|
|
||||||
|
parts_list.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure to be called when the user has edited the track.
|
||||||
|
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
|
||||||
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for TrackEditor {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
musicus/src/import/track_selector.rs
Normal file
122
musicus/src/import/track_selector.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
use super::disc_source::DiscSource;
|
||||||
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use libhandy::prelude::*;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A screen for selecting tracks from a medium.
|
||||||
|
pub struct TrackSelector {
|
||||||
|
source: Rc<DiscSource>,
|
||||||
|
widget: gtk::Box,
|
||||||
|
select_button: gtk::Button,
|
||||||
|
selection: RefCell<Vec<usize>>,
|
||||||
|
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSelector {
|
||||||
|
/// Create a new track selector.
|
||||||
|
pub fn new(source: Rc<DiscSource>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, select_button);
|
||||||
|
get_widget!(builder, gtk::Frame, tracks_frame);
|
||||||
|
|
||||||
|
let track_list = gtk::ListBox::new();
|
||||||
|
track_list.set_selection_mode(gtk::SelectionMode::None);
|
||||||
|
track_list.set_vexpand(false);
|
||||||
|
track_list.show();
|
||||||
|
tracks_frame.add(&track_list);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
source,
|
||||||
|
widget,
|
||||||
|
select_button,
|
||||||
|
selection: RefCell::new(Vec::new()),
|
||||||
|
selected_cb: RefCell::new(None),
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.select_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||||
|
let selection = this.selection.borrow().clone();
|
||||||
|
cb(selection);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (index, track) in this.source.tracks.iter().enumerate() {
|
||||||
|
let check = gtk::CheckButton::new();
|
||||||
|
|
||||||
|
check.connect_toggled(clone!(@strong this => move |check| {
|
||||||
|
let mut selection = this.selection.borrow_mut();
|
||||||
|
if check.get_active() {
|
||||||
|
selection.push(index);
|
||||||
|
} else {
|
||||||
|
if let Some(pos) = selection.iter().position(|part| *part == index) {
|
||||||
|
selection.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selection.is_empty() {
|
||||||
|
this.select_button.set_sensitive(false);
|
||||||
|
} else {
|
||||||
|
this.select_button.set_sensitive(true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let title = format!("Track {}", track.number);
|
||||||
|
|
||||||
|
let row = libhandy::ActionRow::new();
|
||||||
|
row.add_prefix(&check);
|
||||||
|
row.set_activatable_widget(Some(&check));
|
||||||
|
row.set_title(Some(&title));
|
||||||
|
row.show_all();
|
||||||
|
|
||||||
|
track_list.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure to be called when the user has selected tracks. The
|
||||||
|
/// closure will be called with the indices of the selected tracks as its
|
||||||
|
/// argument.
|
||||||
|
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
|
||||||
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for TrackSelector {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
283
musicus/src/import/track_set_editor.rs
Normal file
283
musicus/src/import/track_set_editor.rs
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
use super::disc_source::DiscSource;
|
||||||
|
use super::track_editor::TrackEditor;
|
||||||
|
use super::track_selector::TrackSelector;
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::database::{Recording, Track, TrackSet};
|
||||||
|
use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector};
|
||||||
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
|
use crate::widgets::new_list::List;
|
||||||
|
use gettextrs::gettext;
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use libhandy::prelude::*;
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A track set before being imported.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TrackSetData {
|
||||||
|
pub recording: Recording,
|
||||||
|
pub tracks: Vec<TrackData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A track before being imported.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TrackData {
|
||||||
|
/// Index of the track source within the medium source's tracks.
|
||||||
|
pub track_source: usize,
|
||||||
|
|
||||||
|
/// Actual track data.
|
||||||
|
pub work_parts: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A screen for editing a set of tracks for one recording.
|
||||||
|
pub struct TrackSetEditor {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
source: Rc<DiscSource>,
|
||||||
|
widget: gtk::Box,
|
||||||
|
save_button: gtk::Button,
|
||||||
|
recording_row: libhandy::ActionRow,
|
||||||
|
track_list: List,
|
||||||
|
recording: RefCell<Option<Recording>>,
|
||||||
|
tracks: RefCell<Vec<TrackData>>,
|
||||||
|
done_cb: RefCell<Option<Box<dyn Fn(TrackSetData)>>>,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSetEditor {
|
||||||
|
/// Create a new track set editor.
|
||||||
|
pub fn new(backend: Rc<Backend>, source: Rc<DiscSource>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, save_button);
|
||||||
|
get_widget!(builder, libhandy::ActionRow, recording_row);
|
||||||
|
get_widget!(builder, gtk::Button, select_recording_button);
|
||||||
|
get_widget!(builder, gtk::Button, edit_tracks_button);
|
||||||
|
get_widget!(builder, gtk::Frame, tracks_frame);
|
||||||
|
|
||||||
|
let track_list = List::new(&gettext!("No tracks added"));
|
||||||
|
tracks_frame.add(&track_list.widget);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
source,
|
||||||
|
widget,
|
||||||
|
save_button,
|
||||||
|
recording_row,
|
||||||
|
track_list,
|
||||||
|
recording: RefCell::new(None),
|
||||||
|
tracks: RefCell::new(Vec::new()),
|
||||||
|
done_cb: RefCell::new(None),
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.save_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
if let Some(cb) = &*this.done_cb.borrow() {
|
||||||
|
let data = TrackSetData {
|
||||||
|
recording: this.recording.borrow().clone().unwrap(),
|
||||||
|
tracks: this.tracks.borrow().clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cb(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
select_recording_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let person_selector = PersonSelector::new(this.backend.clone());
|
||||||
|
|
||||||
|
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
|
||||||
|
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
|
||||||
|
|
||||||
|
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
|
||||||
|
let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone());
|
||||||
|
|
||||||
|
recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| {
|
||||||
|
this.recording.replace(Some(recording.clone()));
|
||||||
|
this.recording_selected();
|
||||||
|
|
||||||
|
navigator.clone().pop();
|
||||||
|
navigator.clone().pop();
|
||||||
|
navigator.clone().pop();
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.clone().push(recording_selector);
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.clone().push(work_selector);
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.clone().push(person_selector);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
edit_tracks_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let selector = TrackSelector::new(Rc::clone(&this.source));
|
||||||
|
|
||||||
|
selector.set_selected_cb(clone!(@strong this => move |selection| {
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
|
||||||
|
for index in selection {
|
||||||
|
let data = TrackData {
|
||||||
|
track_source: index,
|
||||||
|
work_parts: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
tracks.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = tracks.len();
|
||||||
|
this.tracks.replace(tracks);
|
||||||
|
this.track_list.update(length);
|
||||||
|
this.autofill_parts();
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.push(selector);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.track_list.set_make_widget(clone!(@strong this => move |index| {
|
||||||
|
let track = &this.tracks.borrow()[index];
|
||||||
|
|
||||||
|
let mut title_parts = Vec::<String>::new();
|
||||||
|
|
||||||
|
if let Some(recording) = &*this.recording.borrow() {
|
||||||
|
for part in &track.work_parts {
|
||||||
|
title_parts.push(recording.work.parts[*part].title.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = if title_parts.is_empty() {
|
||||||
|
gettext("Unknown")
|
||||||
|
} else {
|
||||||
|
title_parts.join(", ")
|
||||||
|
};
|
||||||
|
|
||||||
|
let number = this.source.tracks[track.track_source].number;
|
||||||
|
let subtitle = format!("Track {}", number);
|
||||||
|
|
||||||
|
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
|
||||||
|
let edit_button = gtk::Button::new();
|
||||||
|
edit_button.set_relief(gtk::ReliefStyle::None);
|
||||||
|
edit_button.set_valign(gtk::Align::Center);
|
||||||
|
edit_button.add(&edit_image);
|
||||||
|
|
||||||
|
let row = libhandy::ActionRow::new();
|
||||||
|
row.set_activatable(true);
|
||||||
|
row.set_title(Some(&title));
|
||||||
|
row.set_subtitle(Some(&subtitle));
|
||||||
|
row.add(&edit_button);
|
||||||
|
row.set_activatable_widget(Some(&edit_button));
|
||||||
|
row.show_all();
|
||||||
|
|
||||||
|
edit_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let recording = this.recording.borrow().clone();
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
|
||||||
|
if let (Some(recording), Some(navigator)) = (recording, navigator) {
|
||||||
|
let track = &this.tracks.borrow()[index];
|
||||||
|
|
||||||
|
let editor = TrackEditor::new(recording, track.work_parts.clone());
|
||||||
|
|
||||||
|
editor.set_selected_cb(clone!(@strong this => move |selection| {
|
||||||
|
{
|
||||||
|
let mut tracks = this.tracks.borrow_mut();
|
||||||
|
let mut track = &mut tracks[index];
|
||||||
|
track.work_parts = selection;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update_tracks();
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.push(editor);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
row.upcast()
|
||||||
|
}));
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure to be called when the user has created the track set.
|
||||||
|
pub fn set_done_cb<F: Fn(TrackSetData) + 'static>(&self, cb: F) {
|
||||||
|
self.done_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set everything up after selecting a recording.
|
||||||
|
fn recording_selected(&self) {
|
||||||
|
if let Some(recording) = &*self.recording.borrow() {
|
||||||
|
self.recording_row.set_title(Some(&recording.work.get_title()));
|
||||||
|
self.recording_row.set_subtitle(Some(&recording.get_performers()));
|
||||||
|
self.save_button.set_sensitive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.autofill_parts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Automatically try to put work part information from the selected recording into the
|
||||||
|
/// selected tracks.
|
||||||
|
fn autofill_parts(&self) {
|
||||||
|
if let Some(recording) = &*self.recording.borrow() {
|
||||||
|
let mut tracks = self.tracks.borrow_mut();
|
||||||
|
|
||||||
|
for (index, _) in recording.work.parts.iter().enumerate() {
|
||||||
|
if let Some(mut track) = tracks.get_mut(index) {
|
||||||
|
track.work_parts = vec![index];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_tracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the track list.
|
||||||
|
fn update_tracks(&self) {
|
||||||
|
let length = self.tracks.borrow().len();
|
||||||
|
self.track_list.update(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for TrackSetEditor {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ mod config;
|
||||||
mod database;
|
mod database;
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
mod editors;
|
mod editors;
|
||||||
|
mod import;
|
||||||
mod player;
|
mod player;
|
||||||
mod screens;
|
mod screens;
|
||||||
mod selectors;
|
mod selectors;
|
||||||
|
|
@ -31,6 +32,7 @@ fn main() {
|
||||||
gettextrs::bindtextdomain("musicus", config::LOCALEDIR);
|
gettextrs::bindtextdomain("musicus", config::LOCALEDIR);
|
||||||
gettextrs::textdomain("musicus");
|
gettextrs::textdomain("musicus");
|
||||||
|
|
||||||
|
gstreamer::init().expect("Failed to initialize GStreamer!");
|
||||||
gtk::init().expect("Failed to initialize GTK!");
|
gtk::init().expect("Failed to initialize GTK!");
|
||||||
libhandy::init();
|
libhandy::init();
|
||||||
resources::init().expect("Failed to initialize resources!");
|
resources::init().expect("Failed to initialize resources!");
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,9 @@ run_command(
|
||||||
)
|
)
|
||||||
|
|
||||||
sources = files(
|
sources = files(
|
||||||
'backend/client/mod.rs',
|
|
||||||
'backend/client/ensembles.rs',
|
'backend/client/ensembles.rs',
|
||||||
'backend/client/instruments.rs',
|
'backend/client/instruments.rs',
|
||||||
|
'backend/client/mod.rs',
|
||||||
'backend/client/persons.rs',
|
'backend/client/persons.rs',
|
||||||
'backend/client/recordings.rs',
|
'backend/client/recordings.rs',
|
||||||
'backend/client/works.rs',
|
'backend/client/works.rs',
|
||||||
|
|
@ -44,12 +44,12 @@ sources = files(
|
||||||
'backend/secure.rs',
|
'backend/secure.rs',
|
||||||
'database/ensembles.rs',
|
'database/ensembles.rs',
|
||||||
'database/instruments.rs',
|
'database/instruments.rs',
|
||||||
|
'database/medium.rs',
|
||||||
'database/mod.rs',
|
'database/mod.rs',
|
||||||
'database/persons.rs',
|
'database/persons.rs',
|
||||||
'database/recordings.rs',
|
'database/recordings.rs',
|
||||||
'database/schema.rs',
|
'database/schema.rs',
|
||||||
'database/thread.rs',
|
'database/thread.rs',
|
||||||
'database/tracks.rs',
|
|
||||||
'database/works.rs',
|
'database/works.rs',
|
||||||
'dialogs/about.rs',
|
'dialogs/about.rs',
|
||||||
'dialogs/login_dialog.rs',
|
'dialogs/login_dialog.rs',
|
||||||
|
|
@ -62,8 +62,6 @@ sources = files(
|
||||||
'editors/performance.rs',
|
'editors/performance.rs',
|
||||||
'editors/person.rs',
|
'editors/person.rs',
|
||||||
'editors/recording.rs',
|
'editors/recording.rs',
|
||||||
'editors/track.rs',
|
|
||||||
'editors/tracks.rs',
|
|
||||||
'editors/work.rs',
|
'editors/work.rs',
|
||||||
'editors/work_part.rs',
|
'editors/work_part.rs',
|
||||||
'editors/work_section.rs',
|
'editors/work_section.rs',
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ use std::rc::Rc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlaylistItem {
|
pub struct PlaylistItem {
|
||||||
pub recording: Recording,
|
pub track_set: TrackSet,
|
||||||
pub tracks: Vec<Track>,
|
pub indices: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
|
|
@ -19,11 +19,11 @@ pub struct Player {
|
||||||
current_item: Cell<Option<usize>>,
|
current_item: Cell<Option<usize>>,
|
||||||
current_track: Cell<Option<usize>>,
|
current_track: Cell<Option<usize>>,
|
||||||
playing: Cell<bool>,
|
playing: Cell<bool>,
|
||||||
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>) -> ()>>>,
|
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>,
|
||||||
track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize) -> ()>>>,
|
track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize)>>>,
|
||||||
duration_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>,
|
duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
|
||||||
playing_cbs: RefCell<Vec<Box<dyn Fn(bool) -> ()>>>,
|
playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>,
|
||||||
position_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>,
|
position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
|
|
@ -80,23 +80,23 @@ impl Player {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) -> () + 'static>(&self, cb: F) {
|
pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) + 'static>(&self, cb: F) {
|
||||||
self.playlist_cbs.borrow_mut().push(Box::new(cb));
|
self.playlist_cbs.borrow_mut().push(Box::new(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_track_cb<F: Fn(usize, usize) -> () + 'static>(&self, cb: F) {
|
pub fn add_track_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) {
|
||||||
self.track_cbs.borrow_mut().push(Box::new(cb));
|
self.track_cbs.borrow_mut().push(Box::new(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_duration_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) {
|
pub fn add_duration_cb<F: Fn(u64) + 'static>(&self, cb: F) {
|
||||||
self.duration_cbs.borrow_mut().push(Box::new(cb));
|
self.duration_cbs.borrow_mut().push(Box::new(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_playing_cb<F: Fn(bool) -> () + 'static>(&self, cb: F) {
|
pub fn add_playing_cb<F: Fn(bool) + 'static>(&self, cb: F) {
|
||||||
self.playing_cbs.borrow_mut().push(Box::new(cb));
|
self.playing_cbs.borrow_mut().push(Box::new(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_position_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) {
|
pub fn add_position_cb<F: Fn(u64) + 'static>(&self, cb: F) {
|
||||||
self.position_cbs.borrow_mut().push(Box::new(cb));
|
self.position_cbs.borrow_mut().push(Box::new(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
|
pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
|
||||||
if item.tracks.is_empty() {
|
if item.indices.is_empty() {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"Tried to add playlist item without tracks to playlist!"
|
"Tried to add playlist item without tracks to playlist!"
|
||||||
))
|
))
|
||||||
|
|
@ -199,7 +199,7 @@ impl Player {
|
||||||
current_track -= 1;
|
current_track -= 1;
|
||||||
} else if current_item > 0 {
|
} else if current_item > 0 {
|
||||||
current_item -= 1;
|
current_item -= 1;
|
||||||
current_track = playlist[current_item].tracks.len() - 1;
|
current_track = playlist[current_item].indices.len() - 1;
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("No previous track!"));
|
return Err(anyhow!("No previous track!"));
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +213,7 @@ impl Player {
|
||||||
let playlist = self.playlist.borrow();
|
let playlist = self.playlist.borrow();
|
||||||
let item = &playlist[current_item];
|
let item = &playlist[current_item];
|
||||||
|
|
||||||
current_track + 1 < item.tracks.len() || current_item + 1 < playlist.len()
|
current_track + 1 < item.indices.len() || current_item + 1 < playlist.len()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +231,7 @@ impl Player {
|
||||||
|
|
||||||
let playlist = self.playlist.borrow();
|
let playlist = self.playlist.borrow();
|
||||||
let item = &playlist[current_item];
|
let item = &playlist[current_item];
|
||||||
if current_track + 1 < item.tracks.len() {
|
if current_track + 1 < item.indices.len() {
|
||||||
current_track += 1;
|
current_track += 1;
|
||||||
} else if current_item + 1 < playlist.len() {
|
} else if current_item + 1 < playlist.len() {
|
||||||
current_item += 1;
|
current_item += 1;
|
||||||
|
|
@ -248,15 +248,7 @@ impl Player {
|
||||||
"file://{}",
|
"file://{}",
|
||||||
self.music_library_path
|
self.music_library_path
|
||||||
.join(
|
.join(
|
||||||
self.playlist
|
self.playlist.borrow()[current_item].track_set.tracks[current_track].path.clone(),
|
||||||
.borrow()
|
|
||||||
.get(current_item)
|
|
||||||
.ok_or(anyhow!("Playlist item doesn't exist!"))?
|
|
||||||
.tracks
|
|
||||||
.get(current_track)
|
|
||||||
.ok_or(anyhow!("Track doesn't exist!"))?
|
|
||||||
.file_name
|
|
||||||
.clone(),
|
|
||||||
)
|
)
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
|
||||||
|
|
@ -215,15 +215,17 @@ impl PlayerScreen {
|
||||||
elements.push(PlaylistElement {
|
elements.push(PlaylistElement {
|
||||||
item: item_index,
|
item: item_index,
|
||||||
track: 0,
|
track: 0,
|
||||||
title: item.recording.work.get_title(),
|
title: item.track_set.recording.work.get_title(),
|
||||||
subtitle: Some(item.recording.get_performers()),
|
subtitle: Some(item.track_set.recording.get_performers()),
|
||||||
playable: false,
|
playable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (track_index, track) in item.tracks.iter().enumerate() {
|
for track_index in &item.indices {
|
||||||
|
let track = &item.track_set.tracks[*track_index];
|
||||||
|
|
||||||
let mut parts = Vec::<String>::new();
|
let mut parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
parts.push(item.recording.work.parts[*part].title.clone());
|
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = if parts.is_empty() {
|
let title = if parts.is_empty() {
|
||||||
|
|
@ -234,7 +236,7 @@ impl PlayerScreen {
|
||||||
|
|
||||||
elements.push(PlaylistElement {
|
elements.push(PlaylistElement {
|
||||||
item: item_index,
|
item: item_index,
|
||||||
track: track_index,
|
track: *track_index,
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: None,
|
subtitle: None,
|
||||||
playable: true,
|
playable: true,
|
||||||
|
|
@ -262,20 +264,20 @@ impl PlayerScreen {
|
||||||
next_button.set_sensitive(player.has_next());
|
next_button.set_sensitive(player.has_next());
|
||||||
|
|
||||||
let item = &playlist.borrow()[current_item];
|
let item = &playlist.borrow()[current_item];
|
||||||
let track = &item.tracks[current_track];
|
let track = &item.track_set.tracks[current_track];
|
||||||
|
|
||||||
let mut parts = Vec::<String>::new();
|
let mut parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
parts.push(item.recording.work.parts[*part].title.clone());
|
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = item.recording.work.get_title();
|
let mut title = item.track_set.recording.work.get_title();
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
title = format!("{}: {}", title, parts.join(", "));
|
title = format!("{}: {}", title, parts.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
title_label.set_text(&title);
|
title_label.set_text(&title);
|
||||||
subtitle_label.set_text(&item.recording.get_performers());
|
subtitle_label.set_text(&item.track_set.recording.get_performers());
|
||||||
position_label.set_text("0:00");
|
position_label.set_text("0:00");
|
||||||
|
|
||||||
self_item.replace(current_item);
|
self_item.replace(current_item);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::backend::*;
|
use crate::backend::*;
|
||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::editors::{RecordingEditor, TracksEditor};
|
use crate::editors::RecordingEditor;
|
||||||
use crate::player::*;
|
use crate::player::*;
|
||||||
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
|
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
|
||||||
use gettextrs::gettext;
|
use gettextrs::gettext;
|
||||||
|
|
@ -17,7 +17,7 @@ pub struct RecordingScreen {
|
||||||
recording: Recording,
|
recording: Recording,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
tracks: RefCell<Vec<Track>>,
|
track_sets: RefCell<Vec<TrackSet>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,35 +56,43 @@ impl RecordingScreen {
|
||||||
recording,
|
recording,
|
||||||
widget,
|
widget,
|
||||||
stack,
|
stack,
|
||||||
tracks: RefCell::new(Vec::new()),
|
track_sets: RefCell::new(Vec::new()),
|
||||||
navigator: RefCell::new(None),
|
navigator: RefCell::new(None),
|
||||||
});
|
});
|
||||||
|
|
||||||
list.set_make_widget(clone!(@strong result => move |track: &Track| {
|
list.set_make_widget(clone!(@strong result => move |track_set: &TrackSet| {
|
||||||
let mut title_parts = Vec::<String>::new();
|
|
||||||
for part in &track.work_parts {
|
|
||||||
title_parts.push(result.recording.work.parts[*part].title.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = if title_parts.is_empty() {
|
|
||||||
gettext("Unknown")
|
|
||||||
} else {
|
|
||||||
title_parts.join(", ")
|
|
||||||
};
|
|
||||||
|
|
||||||
let title_label = gtk::Label::new(Some(&title));
|
|
||||||
title_label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
title_label.set_halign(gtk::Align::Start);
|
|
||||||
|
|
||||||
let file_name_label = gtk::Label::new(Some(&track.file_name));
|
|
||||||
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
file_name_label.set_opacity(0.5);
|
|
||||||
file_name_label.set_halign(gtk::Align::Start);
|
|
||||||
|
|
||||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
vbox.set_border_width(6);
|
vbox.set_border_width(6);
|
||||||
vbox.add(&title_label);
|
vbox.set_spacing(6);
|
||||||
vbox.add(&file_name_label);
|
|
||||||
|
for track in &track_set.tracks {
|
||||||
|
let track_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
|
|
||||||
|
let mut title_parts = Vec::<String>::new();
|
||||||
|
for part in &track.work_parts {
|
||||||
|
title_parts.push(result.recording.work.parts[*part].title.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = if title_parts.is_empty() {
|
||||||
|
gettext("Unknown")
|
||||||
|
} else {
|
||||||
|
title_parts.join(", ")
|
||||||
|
};
|
||||||
|
|
||||||
|
let title_label = gtk::Label::new(Some(&title));
|
||||||
|
title_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
title_label.set_halign(gtk::Align::Start);
|
||||||
|
|
||||||
|
let file_name_label = gtk::Label::new(Some(&track.path));
|
||||||
|
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
file_name_label.set_opacity(0.5);
|
||||||
|
file_name_label.set_halign(gtk::Align::Start);
|
||||||
|
|
||||||
|
track_box.add(&title_label);
|
||||||
|
track_box.add(&file_name_label);
|
||||||
|
|
||||||
|
vbox.add(&track_box);
|
||||||
|
}
|
||||||
|
|
||||||
vbox.upcast()
|
vbox.upcast()
|
||||||
}));
|
}));
|
||||||
|
|
@ -97,12 +105,12 @@ impl RecordingScreen {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| {
|
add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| {
|
||||||
if let Some(player) = result.backend.get_player() {
|
// if let Some(player) = result.backend.get_player() {
|
||||||
player.add_item(PlaylistItem {
|
// player.add_item(PlaylistItem {
|
||||||
recording: result.recording.clone(),
|
// track_set: result.track_sets.get(0).unwrap().clone(),
|
||||||
tracks: result.tracks.borrow().clone(),
|
// indices: result.tracks.borrow().clone(),
|
||||||
}).unwrap();
|
// }).unwrap();
|
||||||
}
|
// }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
edit_action.connect_activate(clone!(@strong result => move |_, _| {
|
edit_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||||
|
|
@ -121,33 +129,33 @@ impl RecordingScreen {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||||
let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone());
|
// let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone());
|
||||||
let window = NavigatorWindow::new(editor);
|
// let window = NavigatorWindow::new(editor);
|
||||||
window.show();
|
// window.show();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
|
// clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
|
||||||
clone.backend.library_changed();
|
// clone.backend.library_changed();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let tracks = clone
|
let track_sets = clone
|
||||||
.backend
|
.backend
|
||||||
.db()
|
.db()
|
||||||
.get_tracks(&clone.recording.id)
|
.get_track_sets(&clone.recording.id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
list.show_items(tracks.clone());
|
list.show_items(track_sets.clone());
|
||||||
clone.stack.set_visible_child_name("content");
|
clone.stack.set_visible_child_name("content");
|
||||||
clone.tracks.replace(tracks);
|
clone.track_sets.replace(track_sets);
|
||||||
});
|
});
|
||||||
|
|
||||||
result
|
result
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,16 @@ where
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_selectable(&self, selectable: bool) {
|
||||||
|
let mode = if selectable {
|
||||||
|
gtk::SelectionMode::Single
|
||||||
|
} else {
|
||||||
|
gtk::SelectionMode::None
|
||||||
|
};
|
||||||
|
|
||||||
|
self.widget.set_selection_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
||||||
self.make_widget.replace(Some(Box::new(make_widget)));
|
self.make_widget.replace(Some(Box::new(make_widget)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ pub use navigator::*;
|
||||||
pub mod navigator_window;
|
pub mod navigator_window;
|
||||||
pub use navigator_window::*;
|
pub use navigator_window::*;
|
||||||
|
|
||||||
|
pub mod new_list;
|
||||||
|
|
||||||
pub mod player_bar;
|
pub mod player_bar;
|
||||||
pub use player_bar::*;
|
pub use player_bar::*;
|
||||||
|
|
||||||
|
|
|
||||||
50
musicus/src/widgets/new_list.rs
Normal file
50
musicus/src/widgets/new_list.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
/// A simple list of widgets.
|
||||||
|
pub struct List {
|
||||||
|
pub widget: gtk::ListBox,
|
||||||
|
make_widget: RefCell<Option<Box<dyn Fn(usize) -> gtk::Widget>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl List {
|
||||||
|
/// Create a new list. The list will be empty.
|
||||||
|
pub fn new(placeholder_text: &str) -> Self {
|
||||||
|
let placeholder_label = gtk::Label::new(Some(placeholder_text));
|
||||||
|
placeholder_label.set_margin_top(6);
|
||||||
|
placeholder_label.set_margin_bottom(6);
|
||||||
|
placeholder_label.set_margin_start(6);
|
||||||
|
placeholder_label.set_margin_end(6);
|
||||||
|
placeholder_label.show();
|
||||||
|
|
||||||
|
let widget = gtk::ListBox::new();
|
||||||
|
widget.set_selection_mode(gtk::SelectionMode::None);
|
||||||
|
widget.set_placeholder(Some(&placeholder_label));
|
||||||
|
widget.show();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
widget,
|
||||||
|
make_widget: RefCell::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure to be called to construct widgets for the items.
|
||||||
|
pub fn set_make_widget<F: Fn(usize) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
||||||
|
self.make_widget.replace(Some(Box::new(make_widget)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call the make_widget function for each item. This will automatically
|
||||||
|
/// show all children by indices 0..length.
|
||||||
|
pub fn update(&self, length: usize) {
|
||||||
|
for child in self.widget.get_children() {
|
||||||
|
self.widget.remove(&child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(make_widget) = &*self.make_widget.borrow() {
|
||||||
|
for index in 0..length {
|
||||||
|
let row = make_widget(index);
|
||||||
|
self.widget.insert(&row, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -112,20 +112,20 @@ impl PlayerBar {
|
||||||
next_button.set_sensitive(player.has_next());
|
next_button.set_sensitive(player.has_next());
|
||||||
|
|
||||||
let item = &playlist.borrow()[current_item];
|
let item = &playlist.borrow()[current_item];
|
||||||
let track = &item.tracks[current_track];
|
let track = &item.track_set.tracks[current_track];
|
||||||
|
|
||||||
let mut parts = Vec::<String>::new();
|
let mut parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
parts.push(item.recording.work.parts[*part].title.clone());
|
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = item.recording.work.get_title();
|
let mut title = item.track_set.recording.work.get_title();
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
title = format!("{}: {}", title, parts.join(", "));
|
title = format!("{}: {}", title, parts.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
title_label.set_text(&title);
|
title_label.set_text(&title);
|
||||||
subtitle_label.set_text(&item.recording.get_performers());
|
subtitle_label.set_text(&item.track_set.recording.get_performers());
|
||||||
position_label.set_text("0:00");
|
position_label.set_text("0:00");
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::backend::*;
|
use crate::backend::*;
|
||||||
use crate::dialogs::*;
|
use crate::dialogs::*;
|
||||||
use crate::editors::TracksEditor;
|
use crate::import::SourceSelector;
|
||||||
use crate::screens::*;
|
use crate::screens::*;
|
||||||
use crate::widgets::*;
|
use crate::widgets::*;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
|
|
@ -85,13 +85,17 @@ impl Window {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
add_button.connect_clicked(clone!(@strong result => move |_| {
|
add_button.connect_clicked(clone!(@strong result => move |_| {
|
||||||
let editor = TracksEditor::new(result.backend.clone(), None, Vec::new());
|
// let editor = TracksEditor::new(result.backend.clone(), None, Vec::new());
|
||||||
|
|
||||||
editor.set_callback(clone!(@strong result => move || {
|
// editor.set_callback(clone!(@strong result => move || {
|
||||||
result.reload();
|
// result.reload();
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
let window = NavigatorWindow::new(editor);
|
// let window = NavigatorWindow::new(editor);
|
||||||
|
// window.show();
|
||||||
|
|
||||||
|
let dialog = SourceSelector::new(result.backend.clone());
|
||||||
|
let window = NavigatorWindow::new(dialog);
|
||||||
window.show();
|
window.show();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -107,6 +111,16 @@ impl Window {
|
||||||
result.stack.set_visible_child_name("content");
|
result.stack.set_visible_child_name("content");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// action!(
|
||||||
|
// result.window,
|
||||||
|
// "import-disc",
|
||||||
|
// clone!(@strong result => move |_, _| {
|
||||||
|
// let dialog = ImportDiscDialog::new(result.backend.clone());
|
||||||
|
// let window = NavigatorWindow::new(dialog);
|
||||||
|
// window.show();
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
action!(
|
action!(
|
||||||
result.window,
|
result.window,
|
||||||
"preferences",
|
"preferences",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue