mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Add HTTP client and login support
This commit is contained in:
parent
d20d80d1ac
commit
ea3bd35ffd
16 changed files with 832 additions and 25 deletions
|
|
@ -18,6 +18,10 @@ gtk = { version = "0.9.2", features = ["v3_24"] }
|
||||||
gtk-macros = "0.2.0"
|
gtk-macros = "0.2.0"
|
||||||
gstreamer = "0.16.4"
|
gstreamer = "0.16.4"
|
||||||
gstreamer-player = "0.16.3"
|
gstreamer-player = "0.16.3"
|
||||||
|
isahc = "0.9.12"
|
||||||
libhandy = "0.7.0"
|
libhandy = "0.7.0"
|
||||||
pango = "0.9.1"
|
pango = "0.9.1"
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
|
secret-service = "1.1.1"
|
||||||
|
serde = { version = "1.0.117", features = ["derive"] }
|
||||||
|
serde_json = "1.0.59"
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,9 @@
|
||||||
<default>""</default>
|
<default>""</default>
|
||||||
<summary>Path to the music library folder</summary>
|
<summary>Path to the music library folder</summary>
|
||||||
</key>
|
</key>
|
||||||
|
<key name="server-url" type="s">
|
||||||
|
<default>"https://musicus.johrpan.de"</default>
|
||||||
|
<summary>URL of the Musicus server to use</summary>
|
||||||
|
</key>
|
||||||
</schema>
|
</schema>
|
||||||
</schemalist>
|
</schemalist>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ project('musicus', 'rust',
|
||||||
license: 'AGPLv3+',
|
license: 'AGPLv3+',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dependency('dbus-1', version: '>= 1.3')
|
||||||
dependency('glib-2.0', version: '>= 2.56')
|
dependency('glib-2.0', version: '>= 2.56')
|
||||||
dependency('gio-2.0', version: '>= 2.56')
|
dependency('gio-2.0', version: '>= 2.56')
|
||||||
dependency('gstreamer-1.0', version: '>= 1.12')
|
dependency('gstreamer-1.0', version: '>= 1.12')
|
||||||
dependency('gtk+-3.0', version: '>= 3.24.7')
|
dependency('gtk+-3.0', version: '>= 3.24.7')
|
||||||
|
dependency('libcurl', version: '>= 7.24.0')
|
||||||
dependency('libhandy-1', version: '>= 1.0.0')
|
dependency('libhandy-1', version: '>= 1.0.0')
|
||||||
dependency('pango', version: '>= 1.0')
|
dependency('pango', version: '>= 1.0')
|
||||||
dependency('sqlite3', version: '>= 3.20')
|
dependency('sqlite3', version: '>= 3.20')
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
|
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/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/part_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/part_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>
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
<file preprocess="xml-stripblanks">ui/recording_selector.ui</file>
|
<file preprocess="xml-stripblanks">ui/recording_selector.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file>
|
<file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/section_editor.ui</file>
|
<file preprocess="xml-stripblanks">ui/section_editor.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/tracks_editor.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/window.ui</file>
|
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
||||||
|
|
|
||||||
219
res/ui/login_dialog.ui
Normal file
219
res/ui/login_dialog.ui
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.38.1 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.24"/>
|
||||||
|
<requires lib="libhandy" version="0.0"/>
|
||||||
|
<object class="HdyWindow" id="window">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="modal">True</property>
|
||||||
|
<property name="default-width">350</property>
|
||||||
|
<property name="type-hint">dialog</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkStack" id="stack">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="transition-type">crossfade</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyHeaderBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="title" translatable="yes">Login</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="cancel_button">
|
||||||
|
<property name="label" translatable="yes">Cancel</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="login_button">
|
||||||
|
<property name="label" translatable="yes">Login</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="can-default">True</property>
|
||||||
|
<property name="has-default">True</property>
|
||||||
|
<property name="receives-default">False</property>
|
||||||
|
<style>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="pack-type">end</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkInfoBar" id="info_bar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="message-type">error</property>
|
||||||
|
<property name="revealed">False</property>
|
||||||
|
<child internal-child="action_area">
|
||||||
|
<object class="GtkButtonBox">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<property name="layout-style">end</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child internal-child="content_area">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="spacing">16</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes">The login credentials were wrong!</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<!-- n-columns=2 n-rows=2 -->
|
||||||
|
<object class="GtkGrid">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="border-width">18</property>
|
||||||
|
<property name="row-spacing">12</property>
|
||||||
|
<property name="column-spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="halign">end</property>
|
||||||
|
<property name="label" translatable="yes">Username</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="halign">end</property>
|
||||||
|
<property name="label" translatable="yes">Password</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="username_entry">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="has-focus">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">1</property>
|
||||||
|
<property name="top-attach">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="password_entry">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="visibility">False</property>
|
||||||
|
<property name="activates-default">True</property>
|
||||||
|
<property name="input-purpose">password</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">1</property>
|
||||||
|
<property name="top-attach">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">content</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyHeaderBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="title" translatable="yes">Login</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSpinner">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="active">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">loading</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
|
|
@ -40,7 +40,59 @@
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="HdyPreferencesGroup">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="title" translatable="yes">Server connection</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyActionRow" id="url_row">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="selectable">False</property>
|
||||||
|
<property name="title" translatable="yes">Server URL</property>
|
||||||
|
<property name="activatable-widget">url_button</property>
|
||||||
|
<property name="subtitle" translatable="yes">Not set</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="url_button">
|
||||||
|
<property name="label" translatable="yes">Change</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="HdyActionRow" id="login_row">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="selectable">False</property>
|
||||||
|
<property name="title" translatable="yes">Login credentials</property>
|
||||||
|
<property name="activatable-widget">login_button</property>
|
||||||
|
<property name="subtitle" translatable="yes">Not logged in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="login_button">
|
||||||
|
<property name="label" translatable="yes">Change</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<object class="GtkSizeGroup">
|
||||||
|
<widgets>
|
||||||
|
<widget name="select_music_library_path_button"/>
|
||||||
|
<widget name="url_button"/>
|
||||||
|
<widget name="login_button"/>
|
||||||
|
</widgets>
|
||||||
|
</object>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
|
||||||
96
res/ui/server_dialog.ui
Normal file
96
res/ui/server_dialog.ui
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.38.1 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.24"/>
|
||||||
|
<requires lib="libhandy" version="0.0"/>
|
||||||
|
<object class="HdyWindow" id="window">
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="modal">True</property>
|
||||||
|
<property name="destroy-with-parent">True</property>
|
||||||
|
<property name="type-hint">dialog</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="HdyHeaderBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="title" translatable="yes">Server</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="cancel_button">
|
||||||
|
<property name="label" translatable="yes">Cancel</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="set_button">
|
||||||
|
<property name="label" translatable="yes">Set</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="can-default">True</property>
|
||||||
|
<property name="has-default">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="pack-type">end</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<!-- n-columns=2 n-rows=1 -->
|
||||||
|
<object class="GtkGrid">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="border-width">18</property>
|
||||||
|
<property name="row-spacing">12</property>
|
||||||
|
<property name="column-spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="halign">end</property>
|
||||||
|
<property name="label" translatable="yes">URL</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">0</property>
|
||||||
|
<property name="top-attach">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="url_entry">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="has-focus">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="activates-default">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">1</property>
|
||||||
|
<property name="top-attach">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
use super::database::*;
|
use super::secure;
|
||||||
|
use crate::database::*;
|
||||||
use crate::player::*;
|
use crate::player::*;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use futures_channel::oneshot::Sender;
|
use futures_channel::oneshot::Sender;
|
||||||
use futures_channel::{mpsc, oneshot};
|
use futures_channel::{mpsc, oneshot};
|
||||||
use gio::prelude::*;
|
use gio::prelude::*;
|
||||||
|
use serde::Serialize;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Credentials used for login.
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LoginData {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub enum BackendState {
|
pub enum BackendState {
|
||||||
NoMusicLibrary,
|
NoMusicLibrary,
|
||||||
Loading,
|
Loading,
|
||||||
|
|
@ -50,6 +60,10 @@ pub struct Backend {
|
||||||
state_sender: RefCell<mpsc::Sender<BackendState>>,
|
state_sender: RefCell<mpsc::Sender<BackendState>>,
|
||||||
action_sender: RefCell<Option<std::sync::mpsc::Sender<BackendAction>>>,
|
action_sender: RefCell<Option<std::sync::mpsc::Sender<BackendAction>>>,
|
||||||
settings: gio::Settings,
|
settings: gio::Settings,
|
||||||
|
secrets: secret_service::SecretService,
|
||||||
|
server_url: RefCell<Option<String>>,
|
||||||
|
login_data: RefCell<Option<LoginData>>,
|
||||||
|
token: RefCell<Option<String>>,
|
||||||
music_library_path: RefCell<Option<PathBuf>>,
|
music_library_path: RefCell<Option<PathBuf>>,
|
||||||
player: RefCell<Option<Rc<Player>>>,
|
player: RefCell<Option<Rc<Player>>>,
|
||||||
}
|
}
|
||||||
|
|
@ -57,13 +71,19 @@ pub struct Backend {
|
||||||
impl Backend {
|
impl Backend {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (state_sender, state_stream) = mpsc::channel(1024);
|
let (state_sender, state_stream) = mpsc::channel(1024);
|
||||||
|
let secrets = secret_service::SecretService::new(secret_service::EncryptionType::Dh)
|
||||||
|
.expect("Failed to connect to SecretsService!");
|
||||||
|
|
||||||
Backend {
|
Backend {
|
||||||
state_stream: RefCell::new(state_stream),
|
state_stream: RefCell::new(state_stream),
|
||||||
state_sender: RefCell::new(state_sender),
|
state_sender: RefCell::new(state_sender),
|
||||||
action_sender: RefCell::new(None),
|
action_sender: RefCell::new(None),
|
||||||
settings: gio::Settings::new("de.johrpan.musicus"),
|
settings: gio::Settings::new("de.johrpan.musicus"),
|
||||||
|
secrets,
|
||||||
music_library_path: RefCell::new(None),
|
music_library_path: RefCell::new(None),
|
||||||
|
server_url: RefCell::new(None),
|
||||||
|
login_data: RefCell::new(None),
|
||||||
|
token: RefCell::new(None),
|
||||||
player: RefCell::new(None),
|
player: RefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,13 +92,25 @@ impl Backend {
|
||||||
if let Some(path) = self.settings.get_string("music-library-path") {
|
if let Some(path) = self.settings.get_string("music-library-path") {
|
||||||
if !path.is_empty() {
|
if !path.is_empty() {
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
|
let clone = self.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
self.set_music_library_path_priv(PathBuf::from(path.to_string()))
|
clone
|
||||||
|
.set_music_library_path_priv(PathBuf::from(path.to_string()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(data) = secure::load_login_data().unwrap() {
|
||||||
|
self.login_data.replace(Some(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(url) = self.settings.get_string("server-url") {
|
||||||
|
if !url.is_empty() {
|
||||||
|
self.server_url.replace(Some(url.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_person(&self, person: Person) -> Result<()> {
|
pub async fn update_person(&self, person: Person) -> Result<()> {
|
||||||
|
|
@ -270,6 +302,40 @@ impl Backend {
|
||||||
self.music_library_path.borrow().clone()
|
self.music_library_path.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the currently stored login credentials.
|
||||||
|
pub fn get_login_data(&self) -> Option<LoginData> {
|
||||||
|
self.login_data.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the URL of the Musicus server to connect to.
|
||||||
|
pub fn set_server_url(&self, url: &str) -> Result<()> {
|
||||||
|
self.settings.set_string("server-url", url)?;
|
||||||
|
self.server_url.replace(Some(url.to_string()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the currently used login token.
|
||||||
|
pub fn get_token(&self) -> Option<String> {
|
||||||
|
self.token.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the login token to use. This will be done automatically by the login method.
|
||||||
|
pub fn set_token(&self, token: &str) {
|
||||||
|
self.token.replace(Some(token.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the currently set server URL.
|
||||||
|
pub fn get_server_url(&self) -> Option<String> {
|
||||||
|
self.server_url.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the user credentials to use.
|
||||||
|
pub async fn set_login_data(&self, data: LoginData) -> Result<()> {
|
||||||
|
secure::store_login_data(data.clone()).await?;
|
||||||
|
self.login_data.replace(Some(data));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_player(&self) -> Option<Rc<Player>> {
|
pub fn get_player(&self) -> Option<Rc<Player>> {
|
||||||
self.player.borrow().clone()
|
self.player.borrow().clone()
|
||||||
}
|
}
|
||||||
31
src/backend/client.rs
Normal file
31
src/backend/client.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
use super::Backend;
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use isahc::http::StatusCode;
|
||||||
|
use isahc::prelude::*;
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
/// Try to login a user with the provided credentials and return, wether the login suceeded.
|
||||||
|
pub async fn login(&self) -> Result<bool> {
|
||||||
|
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
|
||||||
|
let data = self.get_login_data().ok_or(anyhow!("No login data set!"))?;
|
||||||
|
|
||||||
|
let request = Request::post(format!("{}/login", server_url))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(serde_json::to_string(&data)?)?;
|
||||||
|
|
||||||
|
let mut response = isahc::send_async(request).await?;
|
||||||
|
|
||||||
|
let success = match response.status() {
|
||||||
|
StatusCode::OK => {
|
||||||
|
let token = response.text_async().await?;
|
||||||
|
self.set_token(&token);
|
||||||
|
println!("{}", &token);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
StatusCode::UNAUTHORIZED => false,
|
||||||
|
_ => bail!("Unexpected response status!"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(success)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/backend/mod.rs
Normal file
7
src/backend/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
pub mod backend;
|
||||||
|
pub use backend::*;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
pub use client::*;
|
||||||
|
|
||||||
|
mod secure;
|
||||||
108
src/backend/secure.rs
Normal file
108
src/backend/secure.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
use super::LoginData;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use futures_channel::oneshot;
|
||||||
|
use secret_service::{Collection, EncryptionType, SecretService};
|
||||||
|
|
||||||
|
/// Savely store the user's current login credentials.
|
||||||
|
pub async fn store_login_data(data: LoginData) -> Result<()> {
|
||||||
|
let (sender, receiver) = oneshot::channel::<Result<()>>();
|
||||||
|
std::thread::spawn(move || sender.send(store_login_data_priv(data)));
|
||||||
|
receiver.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Savely store the user's current login credentials.
|
||||||
|
fn store_login_data_priv(data: LoginData) -> Result<()> {
|
||||||
|
let ss = get_ss()?;
|
||||||
|
let collection = get_collection(&ss)?;
|
||||||
|
|
||||||
|
let key = "musicus-login-data";
|
||||||
|
delete_secrets(&collection, key)?;
|
||||||
|
|
||||||
|
collection
|
||||||
|
.create_item(
|
||||||
|
key,
|
||||||
|
vec![("username", &data.username)],
|
||||||
|
data.password.as_bytes(),
|
||||||
|
true,
|
||||||
|
"text/plain",
|
||||||
|
)
|
||||||
|
.or(Err(anyhow!(
|
||||||
|
"Failed to save login data using SecretService!"
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the login credentials from secret storage.
|
||||||
|
pub fn load_login_data() -> Result<Option<LoginData>> {
|
||||||
|
let ss = get_ss()?;
|
||||||
|
let collection = get_collection(&ss)?;
|
||||||
|
|
||||||
|
let items = collection.get_all_items().or(Err(anyhow!(
|
||||||
|
"Failed to get items from SecretService collection!"
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
let key = "musicus-login-data";
|
||||||
|
let item = items
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.get_label().unwrap_or_default() == key);
|
||||||
|
|
||||||
|
Ok(match item {
|
||||||
|
Some(item) => {
|
||||||
|
let attrs = item.get_attributes().or(Err(anyhow!(
|
||||||
|
"Failed to get attributes for ScretService item!"
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
let username = attrs
|
||||||
|
.iter()
|
||||||
|
.find(|attr| attr.0 == "username")
|
||||||
|
.ok_or(anyhow!("No username in login data!"))?
|
||||||
|
.1
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let password = std::str::from_utf8(
|
||||||
|
&item
|
||||||
|
.get_secret()
|
||||||
|
.or(Err(anyhow!("Failed to get secret from SecretService!")))?,
|
||||||
|
)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Some(LoginData { username, password })
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all stored secrets for the provided key.
|
||||||
|
fn delete_secrets(collection: &Collection, key: &str) -> Result<()> {
|
||||||
|
let items = collection.get_all_items().or(Err(anyhow!(
|
||||||
|
"Failed to get items from SecretService collection!"
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
if item.get_label().unwrap_or_default() == key {
|
||||||
|
item.delete()
|
||||||
|
.or(Err(anyhow!("Failed to delete SecretService item!")))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the SecretService interface.
|
||||||
|
fn get_ss() -> Result<SecretService> {
|
||||||
|
SecretService::new(EncryptionType::Dh).or(Err(anyhow!("Failed to get SecretService!")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default SecretService collection and unlock it.
|
||||||
|
fn get_collection(ss: &SecretService) -> Result<Collection> {
|
||||||
|
let collection = ss
|
||||||
|
.get_default_collection()
|
||||||
|
.or(Err(anyhow!("Failed to get SecretService connection!")))?;
|
||||||
|
|
||||||
|
collection
|
||||||
|
.unlock()
|
||||||
|
.or(Err(anyhow!("Failed to unclock SecretService collection!")))?;
|
||||||
|
|
||||||
|
Ok(collection)
|
||||||
|
}
|
||||||
88
src/dialogs/login_dialog.rs
Normal file
88
src/dialogs/login_dialog.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
use crate::backend::{Backend, LoginData};
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A dialog for entering login credentials.
|
||||||
|
pub struct LoginDialog {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
window: libhandy::Window,
|
||||||
|
stack: gtk::Stack,
|
||||||
|
info_bar: gtk::InfoBar,
|
||||||
|
username_entry: gtk::Entry,
|
||||||
|
password_entry: gtk::Entry,
|
||||||
|
selected_cb: RefCell<Option<Box<dyn Fn(LoginData) -> ()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginDialog {
|
||||||
|
/// Create a new login dialog.
|
||||||
|
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/login_dialog.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, libhandy::Window, window);
|
||||||
|
get_widget!(builder, gtk::Stack, stack);
|
||||||
|
get_widget!(builder, gtk::InfoBar, info_bar);
|
||||||
|
get_widget!(builder, gtk::Button, cancel_button);
|
||||||
|
get_widget!(builder, gtk::Button, login_button);
|
||||||
|
get_widget!(builder, gtk::Entry, username_entry);
|
||||||
|
get_widget!(builder, gtk::Entry, password_entry);
|
||||||
|
|
||||||
|
window.set_transient_for(Some(parent));
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
window,
|
||||||
|
stack,
|
||||||
|
info_bar,
|
||||||
|
username_entry,
|
||||||
|
password_entry,
|
||||||
|
selected_cb: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
cancel_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
this.window.close();
|
||||||
|
}));
|
||||||
|
|
||||||
|
login_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
this.stack.set_visible_child_name("loading");
|
||||||
|
|
||||||
|
let data = LoginData {
|
||||||
|
username: this.username_entry.get_text().to_string(),
|
||||||
|
password: this.password_entry.get_text().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let c = glib::MainContext::default();
|
||||||
|
let clone = this.clone();
|
||||||
|
c.spawn_local(async move {
|
||||||
|
clone.backend.set_login_data(data.clone()).await.unwrap();
|
||||||
|
if clone.backend.login().await.unwrap() {
|
||||||
|
if let Some(cb) = &*clone.selected_cb.borrow() {
|
||||||
|
cb(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.window.close();
|
||||||
|
} else {
|
||||||
|
clone.stack.set_visible_child_name("content");
|
||||||
|
clone.info_bar.set_revealed(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The closure to call when the login succeded.
|
||||||
|
pub fn set_selected_cb<F: Fn(LoginData) -> () + 'static>(&self, cb: F) {
|
||||||
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the login dialog.
|
||||||
|
pub fn show(&self) {
|
||||||
|
self.window.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,9 @@ pub use instrument_editor::*;
|
||||||
pub mod instrument_selector;
|
pub mod instrument_selector;
|
||||||
pub use instrument_selector::*;
|
pub use instrument_selector::*;
|
||||||
|
|
||||||
|
pub mod login_dialog;
|
||||||
|
pub use login_dialog::*;
|
||||||
|
|
||||||
pub mod person_editor;
|
pub mod person_editor;
|
||||||
pub use person_editor::*;
|
pub use person_editor::*;
|
||||||
|
|
||||||
|
|
@ -22,6 +25,9 @@ pub use person_selector::*;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub use preferences::*;
|
pub use preferences::*;
|
||||||
|
|
||||||
|
pub mod server_dialog;
|
||||||
|
pub use server_dialog::*;
|
||||||
|
|
||||||
pub mod recording;
|
pub mod recording;
|
||||||
pub use recording::*;
|
pub use recording::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use super::{LoginDialog, ServerDialog};
|
||||||
use crate::backend::Backend;
|
use crate::backend::Backend;
|
||||||
use gettextrs::gettext;
|
use gettextrs::gettext;
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
|
|
@ -6,47 +7,98 @@ use gtk_macros::get_widget;
|
||||||
use libhandy::prelude::*;
|
use libhandy::prelude::*;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A dialog for configuring the app.
|
||||||
pub struct Preferences {
|
pub struct Preferences {
|
||||||
|
backend: Rc<Backend>,
|
||||||
window: libhandy::Window,
|
window: libhandy::Window,
|
||||||
|
music_library_path_row: libhandy::ActionRow,
|
||||||
|
url_row: libhandy::ActionRow,
|
||||||
|
login_row: libhandy::ActionRow,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preferences {
|
impl Preferences {
|
||||||
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Self {
|
/// Create a new preferences dialog.
|
||||||
|
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/preferences.ui");
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/preferences.ui");
|
||||||
|
|
||||||
get_widget!(builder, libhandy::Window, window);
|
get_widget!(builder, libhandy::Window, window);
|
||||||
get_widget!(builder, libhandy::ActionRow, music_library_path_row);
|
get_widget!(builder, libhandy::ActionRow, music_library_path_row);
|
||||||
get_widget!(builder, gtk::Button, select_music_library_path_button);
|
get_widget!(builder, gtk::Button, select_music_library_path_button);
|
||||||
|
get_widget!(builder, libhandy::ActionRow, url_row);
|
||||||
|
get_widget!(builder, gtk::Button, url_button);
|
||||||
|
get_widget!(builder, libhandy::ActionRow, login_row);
|
||||||
|
get_widget!(builder, gtk::Button, login_button);
|
||||||
|
|
||||||
window.set_transient_for(Some(parent));
|
window.set_transient_for(Some(parent));
|
||||||
|
|
||||||
if let Some(path) = backend.get_music_library_path() {
|
let this = Rc::new(Self {
|
||||||
music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
|
backend,
|
||||||
}
|
window,
|
||||||
|
music_library_path_row,
|
||||||
|
url_row,
|
||||||
|
login_row,
|
||||||
|
});
|
||||||
|
|
||||||
select_music_library_path_button.connect_clicked(
|
// Connect signals and callbacks
|
||||||
clone!(@strong window, @strong backend, @strong music_library_path_row => move |_| {
|
|
||||||
|
select_music_library_path_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
let dialog = gtk::FileChooserNative::new(
|
let dialog = gtk::FileChooserNative::new(
|
||||||
Some(&gettext("Select music library folder")),
|
Some(&gettext("Select music library folder")),
|
||||||
Some(&window), gtk::FileChooserAction::SelectFolder,None, None);
|
Some(&this.window), gtk::FileChooserAction::SelectFolder,None, None);
|
||||||
|
|
||||||
if let gtk::ResponseType::Accept = dialog.run() {
|
if let gtk::ResponseType::Accept = dialog.run() {
|
||||||
if let Some(path) = dialog.get_filename() {
|
if let Some(path) = dialog.get_filename() {
|
||||||
music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
|
this.music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
|
||||||
|
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let backend = backend.clone();
|
let backend = this.backend.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
backend.set_music_library_path(path).await.unwrap();
|
backend.set_music_library_path(path).await.unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
Self { window }
|
url_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let dialog = ServerDialog::new(this.backend.clone(), &this.window);
|
||||||
|
|
||||||
|
dialog.set_selected_cb(clone!(@strong this => move |url| {
|
||||||
|
this.url_row.set_subtitle(Some(&url));
|
||||||
|
}));
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
}));
|
||||||
|
|
||||||
|
login_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let dialog = LoginDialog::new(this.backend.clone(), &this.window);
|
||||||
|
|
||||||
|
dialog.set_selected_cb(clone!(@strong this => move |data| {
|
||||||
|
this.login_row.set_subtitle(Some(&data.username));
|
||||||
|
}));
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
|
||||||
|
if let Some(path) = this.backend.get_music_library_path() {
|
||||||
|
this.music_library_path_row
|
||||||
|
.set_subtitle(Some(path.to_str().unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(url) = this.backend.get_server_url() {
|
||||||
|
this.url_row.set_subtitle(Some(&url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(data) = this.backend.get_login_data() {
|
||||||
|
this.login_row.set_subtitle(Some(&data.username));
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the preferences dialog.
|
||||||
pub fn show(&self) {
|
pub fn show(&self) {
|
||||||
self.window.show();
|
self.window.show();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/dialogs/server_dialog.rs
Normal file
65
src/dialogs/server_dialog.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A dialog for setting up the server.
|
||||||
|
pub struct ServerDialog {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
window: libhandy::Window,
|
||||||
|
url_entry: gtk::Entry,
|
||||||
|
selected_cb: RefCell<Option<Box<dyn Fn(String) -> ()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerDialog {
|
||||||
|
/// Create a new server dialog.
|
||||||
|
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/server_dialog.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, libhandy::Window, window);
|
||||||
|
get_widget!(builder, gtk::Button, cancel_button);
|
||||||
|
get_widget!(builder, gtk::Button, set_button);
|
||||||
|
get_widget!(builder, gtk::Entry, url_entry);
|
||||||
|
|
||||||
|
window.set_transient_for(Some(parent));
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
window,
|
||||||
|
url_entry,
|
||||||
|
selected_cb: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
cancel_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
this.window.close();
|
||||||
|
}));
|
||||||
|
|
||||||
|
set_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let url = this.url_entry.get_text().to_string();
|
||||||
|
this.backend.set_server_url(&url).unwrap();
|
||||||
|
|
||||||
|
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||||
|
cb(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.window.close();
|
||||||
|
}));
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The closure to call when the server was set.
|
||||||
|
pub fn set_selected_cb<F: Fn(String) -> () + 'static>(&self, cb: F) {
|
||||||
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the server dialog.
|
||||||
|
pub fn show(&self) {
|
||||||
|
self.window.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,10 @@ run_command(
|
||||||
)
|
)
|
||||||
|
|
||||||
sources = files(
|
sources = files(
|
||||||
|
'backend/backend.rs',
|
||||||
|
'backend/client.rs',
|
||||||
|
'backend/mod.rs',
|
||||||
|
'backend/secure.rs',
|
||||||
'database/database.rs',
|
'database/database.rs',
|
||||||
'database/mod.rs',
|
'database/mod.rs',
|
||||||
'database/models.rs',
|
'database/models.rs',
|
||||||
|
|
@ -43,10 +47,12 @@ sources = files(
|
||||||
'dialogs/ensemble_selector.rs',
|
'dialogs/ensemble_selector.rs',
|
||||||
'dialogs/instrument_editor.rs',
|
'dialogs/instrument_editor.rs',
|
||||||
'dialogs/instrument_selector.rs',
|
'dialogs/instrument_selector.rs',
|
||||||
|
'dialogs/login_dialog.rs',
|
||||||
'dialogs/mod.rs',
|
'dialogs/mod.rs',
|
||||||
'dialogs/person_editor.rs',
|
'dialogs/person_editor.rs',
|
||||||
'dialogs/person_selector.rs',
|
'dialogs/person_selector.rs',
|
||||||
'dialogs/preferences.rs',
|
'dialogs/preferences.rs',
|
||||||
|
'dialogs/server_dialog.rs',
|
||||||
'dialogs/recording/mod.rs',
|
'dialogs/recording/mod.rs',
|
||||||
'dialogs/recording/performance_editor.rs',
|
'dialogs/recording/performance_editor.rs',
|
||||||
'dialogs/recording/recording_dialog.rs',
|
'dialogs/recording/recording_dialog.rs',
|
||||||
|
|
@ -78,7 +84,6 @@ sources = files(
|
||||||
'widgets/player_bar.rs',
|
'widgets/player_bar.rs',
|
||||||
'widgets/poe_list.rs',
|
'widgets/poe_list.rs',
|
||||||
'widgets/selector_row.rs',
|
'widgets/selector_row.rs',
|
||||||
'backend.rs',
|
|
||||||
'config.rs',
|
'config.rs',
|
||||||
'config.rs.in',
|
'config.rs.in',
|
||||||
'main.rs',
|
'main.rs',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue