mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 19:57:25 +01:00 
			
		
		
		
	Allow to upload instruments
This commit is contained in:
		
							parent
							
								
									13910a664c
								
							
						
					
					
						commit
						9c255d0cfe
					
				
					 10 changed files with 611 additions and 181 deletions
				
			
		|  | @ -8,6 +8,11 @@ | ||||||
|     <property name="modal">True</property> |     <property name="modal">True</property> | ||||||
|     <property name="destroy-with-parent">True</property> |     <property name="destroy-with-parent">True</property> | ||||||
|     <property name="type-hint">dialog</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> |         <child> | ||||||
|           <object class="GtkBox"> |           <object class="GtkBox"> | ||||||
|             <property name="visible">True</property> |             <property name="visible">True</property> | ||||||
|  | @ -49,7 +54,7 @@ | ||||||
|               </packing> |               </packing> | ||||||
|             </child> |             </child> | ||||||
|             <child> |             <child> | ||||||
|           <!-- n-columns=2 n-rows=1 --> |               <!-- n-columns=2 n-rows=2 --> | ||||||
|               <object class="GtkGrid"> |               <object class="GtkGrid"> | ||||||
|                 <property name="visible">True</property> |                 <property name="visible">True</property> | ||||||
|                 <property name="can-focus">False</property> |                 <property name="can-focus">False</property> | ||||||
|  | @ -79,6 +84,83 @@ | ||||||
|                     <property name="top-attach">0</property> |                     <property name="top-attach">0</property> | ||||||
|                   </packing> |                   </packing> | ||||||
|                 </child> |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkLabel"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="halign">end</property> | ||||||
|  |                     <property name="label" translatable="yes">Publish</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="left-attach">0</property> | ||||||
|  |                     <property name="top-attach">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkSwitch" id="upload_switch"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="halign">start</property> | ||||||
|  |                     <property name="active">True</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="left-attach">1</property> | ||||||
|  |                     <property name="top-attach">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkInfoBar" id="info_bar"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="revealed">False</property> | ||||||
|  |                 <child internal-child="action_area"> | ||||||
|  |                   <object class="GtkButtonBox"> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <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">Failed to save instrument!</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> | ||||||
|  |                 <child> | ||||||
|  |                   <placeholder/> | ||||||
|  |                 </child> | ||||||
|               </object> |               </object> | ||||||
|               <packing> |               <packing> | ||||||
|                 <property name="expand">False</property> |                 <property name="expand">False</property> | ||||||
|  | @ -87,6 +169,47 @@ | ||||||
|               </packing> |               </packing> | ||||||
|             </child> |             </child> | ||||||
|           </object> |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="name">content</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkBox"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="orientation">vertical</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="HdyHeaderBar"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="title" translatable="yes">Instrument</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkSpinner"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="vexpand">True</property> | ||||||
|  |                 <property name="active">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="name">loading</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |       </object> | ||||||
|     </child> |     </child> | ||||||
|   </object> |   </object> | ||||||
| </interface> | </interface> | ||||||
|  |  | ||||||
|  | @ -6,8 +6,6 @@ | ||||||
|   <object class="HdyWindow" id="window"> |   <object class="HdyWindow" id="window"> | ||||||
|     <property name="can-focus">False</property> |     <property name="can-focus">False</property> | ||||||
|     <property name="modal">True</property> |     <property name="modal">True</property> | ||||||
|     <property name="default-width">350</property> |  | ||||||
|     <property name="default-height">300</property> |  | ||||||
|     <property name="destroy-with-parent">True</property> |     <property name="destroy-with-parent">True</property> | ||||||
|     <property name="type-hint">dialog</property> |     <property name="type-hint">dialog</property> | ||||||
|     <child> |     <child> | ||||||
|  | @ -47,6 +45,12 @@ | ||||||
|             <property name="visible">True</property> |             <property name="visible">True</property> | ||||||
|             <property name="can-focus">False</property> |             <property name="can-focus">False</property> | ||||||
|             <property name="search-mode-enabled">True</property> |             <property name="search-mode-enabled">True</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="orientation">vertical</property> | ||||||
|  |                 <property name="spacing">6</property> | ||||||
|                 <child> |                 <child> | ||||||
|                   <object class="GtkSearchEntry" id="search_entry"> |                   <object class="GtkSearchEntry" id="search_entry"> | ||||||
|                     <property name="visible">True</property> |                     <property name="visible">True</property> | ||||||
|  | @ -55,6 +59,29 @@ | ||||||
|                     <property name="primary-icon-name">edit-find-symbolic</property> |                     <property name="primary-icon-name">edit-find-symbolic</property> | ||||||
|                     <property name="primary-icon-activatable">False</property> |                     <property name="primary-icon-activatable">False</property> | ||||||
|                     <property name="primary-icon-sensitive">False</property> |                     <property name="primary-icon-sensitive">False</property> | ||||||
|  |                     <property name="placeholder-text" translatable="yes">Search instruments …</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkCheckButton" id="server_check_button"> | ||||||
|  |                     <property name="label" translatable="yes">Show instruments from the Musicus server</property> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="receives-default">False</property> | ||||||
|  |                     <property name="active">True</property> | ||||||
|  |                     <property name="draw-indicator">True</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|               </object> |               </object> | ||||||
|             </child> |             </child> | ||||||
|           </object> |           </object> | ||||||
|  | @ -65,34 +92,116 @@ | ||||||
|           </packing> |           </packing> | ||||||
|         </child> |         </child> | ||||||
|         <child> |         <child> | ||||||
|           <object class="GtkScrolledWindow"> |           <object class="GtkStack" id="stack"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="transition-type">crossfade</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkSpinner"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="active">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="name">loading</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkScrolledWindow" id="scroll"> | ||||||
|                 <property name="visible">True</property> |                 <property name="visible">True</property> | ||||||
|                 <property name="can-focus">True</property> |                 <property name="can-focus">True</property> | ||||||
|             <property name="shadow-type">in</property> |  | ||||||
|                 <child> |                 <child> | ||||||
|               <object class="GtkViewport"> |                   <placeholder/> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="name">content</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|                 <property name="visible">True</property> |                 <property name="visible">True</property> | ||||||
|                 <property name="can-focus">False</property> |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="halign">center</property> | ||||||
|  |                 <property name="valign">center</property> | ||||||
|  |                 <property name="border-width">18</property> | ||||||
|  |                 <property name="orientation">vertical</property> | ||||||
|  |                 <property name="spacing">18</property> | ||||||
|                 <child> |                 <child> | ||||||
|                   <object class="GtkListBox" id="list"> |                   <object class="GtkImage"> | ||||||
|                     <property name="visible">True</property> |                     <property name="visible">True</property> | ||||||
|                     <property name="can-focus">False</property> |                     <property name="can-focus">False</property> | ||||||
|                     <child type="placeholder"> |                     <property name="opacity">0.5019607843137255</property> | ||||||
|  |                     <property name="pixel-size">80</property> | ||||||
|  |                     <property name="icon-name">network-error-symbolic</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|                   <object class="GtkLabel"> |                   <object class="GtkLabel"> | ||||||
|                     <property name="visible">True</property> |                     <property name="visible">True</property> | ||||||
|                     <property name="can-focus">False</property> |                     <property name="can-focus">False</property> | ||||||
|                         <property name="label" translatable="yes">No instruments found.</property> |                     <property name="opacity">0.5019607843137255</property> | ||||||
|                       </object> |                     <property name="label" translatable="yes">An error occured!</property> | ||||||
|                     </child> |                     <attributes> | ||||||
|  |                       <attribute name="size" value="16384"/> | ||||||
|  |                     </attributes> | ||||||
|                   </object> |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkLabel"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="opacity">0.5019607843137255</property> | ||||||
|  |                     <property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property> | ||||||
|  |                     <property name="justify">center</property> | ||||||
|  |                     <property name="wrap">True</property> | ||||||
|  |                     <property name="max-width-chars">40</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">2</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkButton" id="try_again_button"> | ||||||
|  |                     <property name="label" translatable="yes">Try again</property> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="receives-default">True</property> | ||||||
|  |                     <property name="halign">center</property> | ||||||
|  |                     <style> | ||||||
|  |                       <class name="suggested-action"/> | ||||||
|  |                     </style> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">3</property> | ||||||
|  |                   </packing> | ||||||
|                 </child> |                 </child> | ||||||
|               </object> |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="name">error</property> | ||||||
|  |                 <property name="position">2</property> | ||||||
|  |               </packing> | ||||||
|             </child> |             </child> | ||||||
|           </object> |           </object> | ||||||
|           <packing> |           <packing> | ||||||
|             <property name="expand">True</property> |             <property name="expand">True</property> | ||||||
|             <property name="fill">True</property> |             <property name="fill">True</property> | ||||||
|             <property name="position">2</property> |             <property name="position">3</property> | ||||||
|           </packing> |           </packing> | ||||||
|         </child> |         </child> | ||||||
|       </object> |       </object> | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								musicus/src/backend/client/instruments.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								musicus/src/backend/client/instruments.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | use super::Backend; | ||||||
|  | use crate::database::Instrument; | ||||||
|  | use anyhow::Result; | ||||||
|  | 
 | ||||||
|  | impl Backend { | ||||||
|  |     /// Get all available instruments from the server.
 | ||||||
|  |     pub async fn get_instruments(&self) -> Result<Vec<Instrument>> { | ||||||
|  |         let body = self.get("instruments").await?; | ||||||
|  |         let instruments: Vec<Instrument> = serde_json::from_str(&body)?; | ||||||
|  |         Ok(instruments) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Post a new instrument to the server and return the ID.
 | ||||||
|  |     pub async fn post_instrument(&self, data: &Instrument) -> Result<()> { | ||||||
|  |         self.post("instruments", serde_json::to_string(data)?).await?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -7,12 +7,15 @@ use isahc::prelude::*; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
| 
 | 
 | ||||||
| pub mod persons; |  | ||||||
| pub use persons::*; |  | ||||||
| 
 |  | ||||||
| pub mod ensembles; | pub mod ensembles; | ||||||
| pub use ensembles::*; | pub use ensembles::*; | ||||||
| 
 | 
 | ||||||
|  | pub mod instruments; | ||||||
|  | pub use instruments::*; | ||||||
|  | 
 | ||||||
|  | pub mod persons; | ||||||
|  | pub use persons::*; | ||||||
|  | 
 | ||||||
| /// Credentials used for login.
 | /// Credentials used for login.
 | ||||||
| #[derive(Serialize, Debug, Clone)] | #[derive(Serialize, Debug, Clone)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
|  |  | ||||||
|  | @ -1,79 +1,123 @@ | ||||||
| use crate::backend::*; | use crate::backend::Backend; | ||||||
| use crate::database::*; | use crate::database::*; | ||||||
|  | use anyhow::Result; | ||||||
| use glib::clone; | use glib::clone; | ||||||
| use gtk::prelude::*; | use gtk::prelude::*; | ||||||
| use gtk_macros::get_widget; | use gtk_macros::get_widget; | ||||||
|  | use std::cell::RefCell; | ||||||
| use std::rc::Rc; | use std::rc::Rc; | ||||||
| 
 | 
 | ||||||
| pub struct InstrumentEditor<F> | /// A dialog for creating or editing a instrument.
 | ||||||
| where | pub struct InstrumentEditor { | ||||||
|     F: Fn(Instrument) -> () + 'static, |  | ||||||
| { |  | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     window: libhandy::Window, |  | ||||||
|     callback: F, |  | ||||||
|     id: String, |     id: String, | ||||||
|  |     window: libhandy::Window, | ||||||
|  |     stack: gtk::Stack, | ||||||
|  |     info_bar: gtk::InfoBar, | ||||||
|     name_entry: gtk::Entry, |     name_entry: gtk::Entry, | ||||||
|  |     upload_switch: gtk::Switch, | ||||||
|  |     saved_cb: RefCell<Option<Box<dyn Fn(Instrument) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<F> InstrumentEditor<F> | impl InstrumentEditor { | ||||||
| where |     /// Create a new instrument editor and optionally initialize it.
 | ||||||
|     F: Fn(Instrument) -> () + 'static, |  | ||||||
| { |  | ||||||
|     pub fn new<P: IsA<gtk::Window>>( |     pub fn new<P: IsA<gtk::Window>>( | ||||||
|         backend: Rc<Backend>, |         backend: Rc<Backend>, | ||||||
|         parent: &P, |         parent: &P, | ||||||
|         instrument: Option<Instrument>, |         instrument: Option<Instrument>, | ||||||
|         callback: F, |  | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|  |         // Create UI
 | ||||||
|  | 
 | ||||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_editor.ui"); |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_editor.ui"); | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, libhandy::Window, window); |         get_widget!(builder, libhandy::Window, window); | ||||||
|         get_widget!(builder, gtk::Button, cancel_button); |         get_widget!(builder, gtk::Button, cancel_button); | ||||||
|         get_widget!(builder, gtk::Button, save_button); |         get_widget!(builder, gtk::Button, save_button); | ||||||
|  |         get_widget!(builder, gtk::Stack, stack); | ||||||
|  |         get_widget!(builder, gtk::InfoBar, info_bar); | ||||||
|         get_widget!(builder, gtk::Entry, name_entry); |         get_widget!(builder, gtk::Entry, name_entry); | ||||||
|  |         get_widget!(builder, gtk::Switch, upload_switch); | ||||||
| 
 | 
 | ||||||
|         let id = match instrument { |         let id = match instrument { | ||||||
|             Some(instrument) => { |             Some(instrument) => { | ||||||
|                 name_entry.set_text(&instrument.name); |                 name_entry.set_text(&instrument.name); | ||||||
|  | 
 | ||||||
|                 instrument.id |                 instrument.id | ||||||
|             } |             } | ||||||
|             None => generate_id(), |             None => generate_id(), | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let result = Rc::new(InstrumentEditor { |         let this = Rc::new(Self { | ||||||
|             backend: backend, |             backend, | ||||||
|             window: window, |             id, | ||||||
|             callback: callback, |             window, | ||||||
|             id: id, |             stack, | ||||||
|             name_entry: name_entry, |             info_bar, | ||||||
|  |             name_entry, | ||||||
|  |             upload_switch, | ||||||
|  |             saved_cb: RefCell::new(None), | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         cancel_button.connect_clicked(clone!(@strong result => move |_| { |         // Connect signals and callbacks
 | ||||||
|             result.window.close(); | 
 | ||||||
|  |         cancel_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|  |             this.window.close(); | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         save_button.connect_clicked(clone!(@strong result => move |_| { |         save_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|             let instrument = Instrument { |             let context = glib::MainContext::default(); | ||||||
|                 id: result.id.clone(), |             let clone = this.clone(); | ||||||
|                 name: result.name_entry.get_text().to_string(), |             context.spawn_local(async move { | ||||||
|             }; |                 clone.stack.set_visible_child_name("loading"); | ||||||
| 
 |                 match clone.clone().save().await { | ||||||
|             let c = glib::MainContext::default(); |                     Ok(_) => { | ||||||
|             let clone = result.clone(); |  | ||||||
|             c.spawn_local(async move { |  | ||||||
|                 clone.backend.db().update_instrument(instrument.clone()).await.unwrap(); |  | ||||||
|                         clone.window.close(); |                         clone.window.close(); | ||||||
|                 (clone.callback)(instrument.clone()); |                     } | ||||||
|             }); |                     Err(_) => { | ||||||
|         })); |                         clone.info_bar.set_revealed(true); | ||||||
| 
 |                         clone.stack.set_visible_child_name("content"); | ||||||
|         result.window.set_transient_for(Some(parent)); |                     } | ||||||
| 
 |  | ||||||
|         result |  | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  |             }); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         this.window.set_transient_for(Some(parent)); | ||||||
|  | 
 | ||||||
|  |         this | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Set the closure to be called if the instrument was saved.
 | ||||||
|  |     pub fn set_saved_cb<F: Fn(Instrument) -> () + 'static>(&self, cb: F) { | ||||||
|  |         self.saved_cb.replace(Some(Box::new(cb))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Show the instrument editor.
 | ||||||
|     pub fn show(&self) { |     pub fn show(&self) { | ||||||
|         self.window.show(); |         self.window.show(); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /// Save the instrument and possibly upload it to the server.
 | ||||||
|  |     async fn save(self: Rc<Self>) -> Result<()> { | ||||||
|  |         let name = self.name_entry.get_text().to_string(); | ||||||
|  | 
 | ||||||
|  |         let instrument = Instrument { | ||||||
|  |             id: self.id.clone(), | ||||||
|  |             name, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let upload = self.upload_switch.get_active(); | ||||||
|  |         if upload { | ||||||
|  |             self.backend.post_instrument(&instrument).await?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.backend.db().update_instrument(instrument.clone()).await?; | ||||||
|  |         self.backend.library_changed(); | ||||||
|  | 
 | ||||||
|  |         if let Some(cb) = &*self.saved_cb.borrow() { | ||||||
|  |             cb(instrument.clone()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,111 +1,161 @@ | ||||||
| use super::InstrumentEditor; | use super::InstrumentEditor; | ||||||
| use crate::backend::Backend; | use crate::backend::Backend; | ||||||
| use crate::database::*; | use crate::database::Instrument; | ||||||
| use crate::widgets::*; | use crate::widgets::List; | ||||||
|  | use gettextrs::gettext; | ||||||
| use gio::prelude::*; | use gio::prelude::*; | ||||||
| use glib::clone; | use glib::clone; | ||||||
| use gtk::prelude::*; | use gtk::prelude::*; | ||||||
| use gtk_macros::get_widget; | use gtk_macros::get_widget; | ||||||
| use std::convert::TryInto; | use std::cell::RefCell; | ||||||
| use std::rc::Rc; | use std::rc::Rc; | ||||||
| 
 | 
 | ||||||
| pub struct InstrumentSelector<F> | /// A dialog for selecting a instrument.
 | ||||||
| where | pub struct InstrumentSelector { | ||||||
|     F: Fn(Instrument) -> () + 'static, |  | ||||||
| { |  | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     window: libhandy::Window, |     window: libhandy::Window, | ||||||
|     callback: F, |     server_check_button: gtk::CheckButton, | ||||||
|     list: gtk::ListBox, |     stack: gtk::Stack, | ||||||
|     search_entry: gtk::SearchEntry, |     list: Rc<List<Instrument>>, | ||||||
|  |     selected_cb: RefCell<Option<Box<dyn Fn(Instrument) -> ()>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<F> InstrumentSelector<F> | impl InstrumentSelector { | ||||||
| where |     pub fn new<P>(backend: Rc<Backend>, parent: &P) -> Rc<Self> | ||||||
|     F: Fn(Instrument) -> () + 'static, |     where | ||||||
| { |         P: IsA<gtk::Window>, | ||||||
|     pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> { |     { | ||||||
|  |         // Create UI
 | ||||||
|  | 
 | ||||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui"); |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui"); | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, libhandy::Window, window); |         get_widget!(builder, libhandy::Window, window); | ||||||
|         get_widget!(builder, gtk::Button, add_button); |         get_widget!(builder, gtk::Button, add_button); | ||||||
|  |         get_widget!(builder, gtk::CheckButton, server_check_button); | ||||||
|         get_widget!(builder, gtk::SearchEntry, search_entry); |         get_widget!(builder, gtk::SearchEntry, search_entry); | ||||||
|         get_widget!(builder, gtk::ListBox, list); |         get_widget!(builder, gtk::Stack, stack); | ||||||
|  |         get_widget!(builder, gtk::ScrolledWindow, scroll); | ||||||
|  |         get_widget!(builder, gtk::Button, try_again_button); | ||||||
| 
 | 
 | ||||||
|         let result = Rc::new(InstrumentSelector { |         window.set_transient_for(Some(parent)); | ||||||
|             backend: backend, | 
 | ||||||
|             window: window, |         let list = List::<Instrument>::new(&gettext("No instruments found.")); | ||||||
|             callback: callback, |         scroll.add(&list.widget); | ||||||
|             search_entry: search_entry, | 
 | ||||||
|             list: list, |         let this = Rc::new(Self { | ||||||
|  |             backend, | ||||||
|  |             window, | ||||||
|  |             server_check_button, | ||||||
|  |             stack, | ||||||
|  |             list, | ||||||
|  |             selected_cb: RefCell::new(None), | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         let c = glib::MainContext::default(); |         // Connect signals and callbacks
 | ||||||
|         let clone = result.clone(); |  | ||||||
|         c.spawn_local(async move { |  | ||||||
|             let instruments = clone.backend.db().get_instruments().await.unwrap(); |  | ||||||
| 
 | 
 | ||||||
|             for (index, instrument) in instruments.iter().enumerate() { |         add_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|  |             let editor = InstrumentEditor::new( | ||||||
|  |                 this.backend.clone(), | ||||||
|  |                 &this.window, | ||||||
|  |                 None, | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             editor.set_saved_cb(clone!(@strong this => move |instrument| { | ||||||
|  |                 if let Some(cb) = &*this.selected_cb.borrow() { | ||||||
|  |                     cb(instrument); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 this.window.close(); | ||||||
|  |             })); | ||||||
|  | 
 | ||||||
|  |             editor.show(); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         search_entry.connect_search_changed(clone!(@strong this => move |_| { | ||||||
|  |             this.list.invalidate_filter(); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         let load_online = Rc::new(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 clone.backend.get_instruments().await { | ||||||
|  |                     Ok(instruments) => { | ||||||
|  |                         clone.list.show_items(instruments); | ||||||
|  |                         clone.stack.set_visible_child_name("content"); | ||||||
|  |                     } | ||||||
|  |                     Err(_) => { | ||||||
|  |                         clone.list.show_items(Vec::new()); | ||||||
|  |                         clone.stack.set_visible_child_name("error"); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         let load_local = Rc::new(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 { | ||||||
|  |                 let instruments = clone.backend.db().get_instruments().await.unwrap(); | ||||||
|  |                 clone.list.show_items(instruments); | ||||||
|  |                 clone.stack.set_visible_child_name("content"); | ||||||
|  |             }); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         this.server_check_button.connect_toggled( | ||||||
|  |             clone!(@strong this, @strong load_local, @strong load_online => move |_| { | ||||||
|  |                 if this.server_check_button.get_active() { | ||||||
|  |                     load_online(); | ||||||
|  |                 } else { | ||||||
|  |                     load_local(); | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         this.list.set_make_widget(|instrument: &Instrument| { | ||||||
|             let label = gtk::Label::new(Some(&instrument.name)); |             let label = gtk::Label::new(Some(&instrument.name)); | ||||||
|             label.set_halign(gtk::Align::Start); |             label.set_halign(gtk::Align::Start); | ||||||
|             label.set_margin_start(6); |             label.set_margin_start(6); | ||||||
|             label.set_margin_end(6); |             label.set_margin_end(6); | ||||||
|             label.set_margin_top(6); |             label.set_margin_top(6); | ||||||
|             label.set_margin_bottom(6); |             label.set_margin_bottom(6); | ||||||
| 
 |             label.upcast() | ||||||
|                 let row = SelectorRow::new(index.try_into().unwrap(), &label); |  | ||||||
|                 row.show_all(); |  | ||||||
|                 clone.list.insert(&row, -1); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             clone.list.connect_row_activated( |  | ||||||
|                 clone!(@strong clone, @strong instruments => move |_, row| { |  | ||||||
|                     clone.window.close(); |  | ||||||
|                     let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap(); |  | ||||||
|                     let index: usize = row.get_index().try_into().unwrap(); |  | ||||||
|                     (clone.callback)(instruments[index].clone()); |  | ||||||
|                 }), |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             clone |  | ||||||
|                 .list |  | ||||||
|                 .set_filter_func(Some(Box::new(clone!(@strong clone => move |row| { |  | ||||||
|                     let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap(); |  | ||||||
|                     let index: usize = row.get_index().try_into().unwrap(); |  | ||||||
|                     let search = clone.search_entry.get_text().to_string(); |  | ||||||
| 
 |  | ||||||
|                     search.is_empty() || instruments[index] |  | ||||||
|                         .name |  | ||||||
|                         .to_lowercase() |  | ||||||
|                         .contains(&clone.search_entry.get_text().to_string().to_lowercase()) |  | ||||||
|                 })))); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         result |         this.list | ||||||
|             .search_entry |             .set_filter(clone!(@strong search_entry => move |instrument: &Instrument| { | ||||||
|             .connect_search_changed(clone!(@strong result => move |_| { |                 let search = search_entry.get_text().to_string().to_lowercase(); | ||||||
|                 result.list.invalidate_filter(); |                 search.is_empty() || instrument.name.contains(&search) | ||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
|         add_button.connect_clicked(clone!(@strong result => move |_| { |         this.list.set_selected(clone!(@strong this => move |work| { | ||||||
|             let editor = InstrumentEditor::new( |             if let Some(cb) = &*this.selected_cb.borrow() { | ||||||
|                 result.backend.clone(), |                 cb(work.clone()); | ||||||
|                 &result.window, |  | ||||||
|                 None, |  | ||||||
|                 clone!(@strong result => move |instrument| { |  | ||||||
|                     result.window.close(); |  | ||||||
|                     (result.callback)(instrument); |  | ||||||
|                 }), |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             editor.show(); |  | ||||||
|         })); |  | ||||||
| 
 |  | ||||||
|         result.window.set_transient_for(Some(parent)); |  | ||||||
| 
 |  | ||||||
|         result |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             this.window.close(); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         try_again_button.connect_clicked(clone!(@strong load_online => move |_| { | ||||||
|  |             load_online(); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         // Initialize
 | ||||||
|  |         load_online(); | ||||||
|  | 
 | ||||||
|  |         this | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Set the closure to be called when the user has selected a instrument.
 | ||||||
|  |     pub fn set_selected_cb<F: Fn(Instrument) -> () + 'static>(&self, cb: F) { | ||||||
|  |         self.selected_cb.replace(Some(Box::new(cb))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Show the instrument selector.
 | ||||||
|     pub fn show(&self) { |     pub fn show(&self) { | ||||||
|         self.window.show(); |         self.window.show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -107,10 +107,14 @@ impl PerformanceEditor { | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         role_button.connect_clicked(clone!(@strong this => move |_| { |         role_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|             InstrumentSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |role| { |             let dialog = InstrumentSelector::new(this.backend.clone(), &this.window); | ||||||
|  | 
 | ||||||
|  |             dialog.set_selected_cb(clone!(@strong this => move |role| { | ||||||
|                 this.show_role(Some(&role)); |                 this.show_role(Some(&role)); | ||||||
|                 this.role.replace(Some(role)); |                 this.role.replace(Some(role)); | ||||||
|             })).show(); |             })); | ||||||
|  | 
 | ||||||
|  |             dialog.show(); | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         this.reset_role_button |         this.reset_role_button | ||||||
|  |  | ||||||
|  | @ -178,7 +178,9 @@ impl WorkEditor { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         add_instrument_button.connect_clicked(clone!(@strong this => move |_| { |         add_instrument_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|             InstrumentSelector::new(this.backend.clone(), &this.parent, clone!(@strong this => move |instrument| { |             let dialog = InstrumentSelector::new(this.backend.clone(), &this.parent); | ||||||
|  | 
 | ||||||
|  |             dialog.set_selected_cb(clone!(@strong this => move |instrument| { | ||||||
|                 let mut instruments = this.instruments.borrow_mut(); |                 let mut instruments = this.instruments.borrow_mut(); | ||||||
| 
 | 
 | ||||||
|                 let index = match this.instrument_list.get_selected_index() { |                 let index = match this.instrument_list.get_selected_index() { | ||||||
|  | @ -189,7 +191,9 @@ impl WorkEditor { | ||||||
|                 instruments.insert(index, instrument); |                 instruments.insert(index, instrument); | ||||||
|                 this.instrument_list.show_items(instruments.clone()); |                 this.instrument_list.show_items(instruments.clone()); | ||||||
|                 this.instrument_list.select_index(index); |                 this.instrument_list.select_index(index); | ||||||
|             })).show(); |             })); | ||||||
|  | 
 | ||||||
|  |             dialog.show(); | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         remove_instrument_button.connect_clicked(clone!(@strong this => move |_| { |         remove_instrument_button.connect_clicked(clone!(@strong this => move |_| { | ||||||
|  |  | ||||||
|  | @ -35,6 +35,10 @@ async fn main() -> std::io::Result<()> { | ||||||
|             .service(update_ensemble) |             .service(update_ensemble) | ||||||
|             .service(delete_ensemble) |             .service(delete_ensemble) | ||||||
|             .service(get_ensembles) |             .service(get_ensembles) | ||||||
|  |             .service(get_instrument) | ||||||
|  |             .service(update_instrument) | ||||||
|  |             .service(delete_instrument) | ||||||
|  |             .service(get_instruments) | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     server.bind("127.0.0.1:8087")?.run().await |     server.bind("127.0.0.1:8087")?.run().await | ||||||
|  |  | ||||||
|  | @ -0,0 +1,71 @@ | ||||||
|  | use super::authenticate; | ||||||
|  | use crate::database; | ||||||
|  | use crate::database::{DbPool, Instrument}; | ||||||
|  | use crate::error::ServerError; | ||||||
|  | use actix_web::{delete, get, post, web, HttpResponse}; | ||||||
|  | use actix_web_httpauth::extractors::bearer::BearerAuth; | ||||||
|  | 
 | ||||||
|  | /// Get an existing instrument.
 | ||||||
|  | #[get("/instruments/{id}")] | ||||||
|  | pub async fn get_instrument( | ||||||
|  |     db: web::Data<DbPool>, | ||||||
|  |     id: web::Path<String>, | ||||||
|  | ) -> Result<HttpResponse, ServerError> { | ||||||
|  |     let data = web::block(move || { | ||||||
|  |         let conn = db.into_inner().get()?; | ||||||
|  |         database::get_instrument(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) | ||||||
|  |     }) | ||||||
|  |     .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(HttpResponse::Ok().json(data)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Add a new instrument or update an existin one. The user must be authorized to do that.
 | ||||||
|  | #[post("/instruments")] | ||||||
|  | pub async fn update_instrument( | ||||||
|  |     auth: BearerAuth, | ||||||
|  |     db: web::Data<DbPool>, | ||||||
|  |     data: web::Json<Instrument>, | ||||||
|  | ) -> Result<HttpResponse, ServerError> { | ||||||
|  |     web::block(move || { | ||||||
|  |         let conn = db.into_inner().get()?; | ||||||
|  |         let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; | ||||||
|  | 
 | ||||||
|  |         database::update_instrument(&conn, &data.into_inner(), &user)?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     }) | ||||||
|  |     .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(HttpResponse::Ok().finish()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[get("/instruments")] | ||||||
|  | pub async fn get_instruments(db: web::Data<DbPool>) -> Result<HttpResponse, ServerError> { | ||||||
|  |     let data = web::block(move || { | ||||||
|  |         let conn = db.into_inner().get()?; | ||||||
|  |         Ok(database::get_instruments(&conn)?) | ||||||
|  |     }) | ||||||
|  |     .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(HttpResponse::Ok().json(data)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[delete("/instruments/{id}")] | ||||||
|  | pub async fn delete_instrument( | ||||||
|  |     auth: BearerAuth, | ||||||
|  |     db: web::Data<DbPool>, | ||||||
|  |     id: web::Path<String>, | ||||||
|  | ) -> Result<HttpResponse, ServerError> { | ||||||
|  |     web::block(move || { | ||||||
|  |         let conn = db.into_inner().get()?; | ||||||
|  |         let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; | ||||||
|  | 
 | ||||||
|  |         database::delete_instrument(&conn, &id.into_inner(), &user)?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     }) | ||||||
|  |     .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(HttpResponse::Ok().finish()) | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn