mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 19:57:25 +01:00 
			
		
		
		
	Allow to upload recordings
This commit is contained in:
		
							parent
							
								
									ed14988a56
								
							
						
					
					
						commit
						bec0dfbf56
					
				
					 18 changed files with 880 additions and 218 deletions
				
			
		|  | @ -3,7 +3,11 @@ | ||||||
| <interface> | <interface> | ||||||
|   <requires lib="gtk+" version="3.22"/> |   <requires lib="gtk+" version="3.22"/> | ||||||
|   <requires lib="libhandy" version="0.0"/> |   <requires lib="libhandy" version="0.0"/> | ||||||
|   <object class="GtkBox" id="widget"> |   <object class="GtkStack" id="widget"> | ||||||
|  |     <property name="visible">True</property> | ||||||
|  |     <property name="can-focus">False</property> | ||||||
|  |     <child> | ||||||
|  |       <object class="GtkBox"> | ||||||
|         <property name="visible">True</property> |         <property name="visible">True</property> | ||||||
|         <property name="can-focus">False</property> |         <property name="can-focus">False</property> | ||||||
|         <property name="orientation">vertical</property> |         <property name="orientation">vertical</property> | ||||||
|  | @ -43,12 +47,28 @@ | ||||||
|             <property name="position">0</property> |             <property name="position">0</property> | ||||||
|           </packing> |           </packing> | ||||||
|         </child> |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkInfoBar" id="info_bar"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="revealed">False</property> | ||||||
|  |             <child> | ||||||
|  |               <placeholder/> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|         <child> |         <child> | ||||||
|           <object class="GtkNotebook"> |           <object class="GtkNotebook"> | ||||||
|             <property name="visible">True</property> |             <property name="visible">True</property> | ||||||
|             <property name="can-focus">True</property> |             <property name="can-focus">True</property> | ||||||
|  |             <property name="show-border">False</property> | ||||||
|             <child> |             <child> | ||||||
|           <!-- n-columns=2 n-rows=2 --> |               <!-- n-columns=2 n-rows=3 --> | ||||||
|               <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> | ||||||
|  | @ -110,6 +130,30 @@ | ||||||
|                     <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">2</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkSwitch" id="upload_switch"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="halign">start</property> | ||||||
|  |                     <property name="active">True</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="left-attach">1</property> | ||||||
|  |                     <property name="top-attach">2</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|               </object> |               </object> | ||||||
|             </child> |             </child> | ||||||
|             <child type="tab"> |             <child type="tab"> | ||||||
|  | @ -234,6 +278,47 @@ | ||||||
|           <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> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |       </object> | ||||||
|  |       <packing> | ||||||
|  |         <property name="name">content</property> | ||||||
|  |       </packing> | ||||||
|  |     </child> | ||||||
|  |     <child> | ||||||
|  |       <object class="GtkBox"> | ||||||
|  |         <property name="visible">True</property> | ||||||
|  |         <property name="can-focus">False</property> | ||||||
|  |         <property name="orientation">vertical</property> | ||||||
|  |         <child> | ||||||
|  |           <object class="HdyHeaderBar"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="title" translatable="yes">Recording</property> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">0</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkSpinner"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="vexpand">True</property> | ||||||
|  |             <property name="active">True</property> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |       </object> | ||||||
|  |       <packing> | ||||||
|  |         <property name="name">loading</property> | ||||||
|         <property name="position">1</property> |         <property name="position">1</property> | ||||||
|       </packing> |       </packing> | ||||||
|     </child> |     </child> | ||||||
|  |  | ||||||
|  | @ -71,6 +71,171 @@ | ||||||
|             <property name="position">0</property> |             <property name="position">0</property> | ||||||
|           </packing> |           </packing> | ||||||
|         </child> |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="HdySearchBar"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="search-mode-enabled">True</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="orientation">vertical</property> | ||||||
|  |                 <property name="spacing">6</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkSearchEntry" id="search_entry"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="primary-icon-name">edit-find-symbolic</property> | ||||||
|  |                     <property name="primary-icon-activatable">False</property> | ||||||
|  |                     <property name="primary-icon-sensitive">False</property> | ||||||
|  |                     <property name="placeholder-text" translatable="yes">Search composers …</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkCheckButton" id="server_check_button"> | ||||||
|  |                     <property name="label" translatable="yes">Show works from the server</property> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="receives-default">False</property> | ||||||
|  |                     <property name="active">True</property> | ||||||
|  |                     <property name="draw-indicator">True</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkStack" id="stack"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="hhomogeneous">False</property> | ||||||
|  |             <property name="transition-type">crossfade</property> | ||||||
|  |             <property name="interpolate-size">True</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkSpinner"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="active">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="name">loading</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkScrolledWindow" id="scroll"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <child> | ||||||
|  |                   <placeholder/> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="name">content</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="halign">center</property> | ||||||
|  |                 <property name="valign">center</property> | ||||||
|  |                 <property name="border-width">18</property> | ||||||
|  |                 <property name="orientation">vertical</property> | ||||||
|  |                 <property name="spacing">18</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkImage"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="opacity">0.5019607843137255</property> | ||||||
|  |                     <property name="pixel-size">80</property> | ||||||
|  |                     <property name="icon-name">network-error-symbolic</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkLabel"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="opacity">0.5019607843137255</property> | ||||||
|  |                     <property name="label" translatable="yes">An error occured!</property> | ||||||
|  |                     <attributes> | ||||||
|  |                       <attribute name="size" value="16384"/> | ||||||
|  |                     </attributes> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkLabel"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="opacity">0.5019607843137255</property> | ||||||
|  |                     <property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property> | ||||||
|  |                     <property name="justify">center</property> | ||||||
|  |                     <property name="wrap">True</property> | ||||||
|  |                     <property name="max-width-chars">40</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">2</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkButton" id="try_again_button"> | ||||||
|  |                     <property name="label" translatable="yes">Try again</property> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="receives-default">True</property> | ||||||
|  |                     <property name="halign">center</property> | ||||||
|  |                     <style> | ||||||
|  |                       <class name="suggested-action"/> | ||||||
|  |                     </style> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">3</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="name">error</property> | ||||||
|  |                 <property name="position">2</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">True</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">2</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|       </object> |       </object> | ||||||
|       <packing> |       <packing> | ||||||
|         <property name="name">sidebar</property> |         <property name="name">sidebar</property> | ||||||
|  |  | ||||||
|  | @ -38,6 +38,9 @@ | ||||||
|       <object class="GtkStack" id="stack"> |       <object class="GtkStack" id="stack"> | ||||||
|         <property name="visible">True</property> |         <property name="visible">True</property> | ||||||
|         <property name="can-focus">False</property> |         <property name="can-focus">False</property> | ||||||
|  |         <property name="hhomogeneous">False</property> | ||||||
|  |         <property name="transition-type">crossfade</property> | ||||||
|  |         <property name="interpolate-size">True</property> | ||||||
|         <child> |         <child> | ||||||
|           <object class="GtkSpinner"> |           <object class="GtkSpinner"> | ||||||
|             <property name="visible">True</property> |             <property name="visible">True</property> | ||||||
|  | @ -48,6 +51,97 @@ | ||||||
|             <property name="name">loading</property> |             <property name="name">loading</property> | ||||||
|           </packing> |           </packing> | ||||||
|         </child> |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkScrolledWindow" id="scroll"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">True</property> | ||||||
|  |             <child> | ||||||
|  |               <placeholder/> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="name">content</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkBox"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="halign">center</property> | ||||||
|  |             <property name="valign">center</property> | ||||||
|  |             <property name="border-width">18</property> | ||||||
|  |             <property name="orientation">vertical</property> | ||||||
|  |             <property name="spacing">18</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkImage"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="opacity">0.5019607843137255</property> | ||||||
|  |                 <property name="pixel-size">80</property> | ||||||
|  |                 <property name="icon-name">network-error-symbolic</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkLabel"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="opacity">0.5019607843137255</property> | ||||||
|  |                 <property name="label" translatable="yes">An error occured!</property> | ||||||
|  |                 <attributes> | ||||||
|  |                   <attribute name="size" value="16384"/> | ||||||
|  |                 </attributes> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkLabel"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="opacity">0.5019607843137255</property> | ||||||
|  |                 <property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property> | ||||||
|  |                 <property name="justify">center</property> | ||||||
|  |                 <property name="wrap">True</property> | ||||||
|  |                 <property name="max-width-chars">40</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">2</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="try_again_button"> | ||||||
|  |                 <property name="label" translatable="yes">Try again</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |                 <property name="halign">center</property> | ||||||
|  |                 <style> | ||||||
|  |                   <class name="suggested-action"/> | ||||||
|  |                 </style> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">3</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="name">error</property> | ||||||
|  |             <property name="position">2</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|       </object> |       </object> | ||||||
|       <packing> |       <packing> | ||||||
|         <property name="expand">True</property> |         <property name="expand">True</property> | ||||||
|  |  | ||||||
|  | @ -52,6 +52,26 @@ | ||||||
|             <property name="visible">True</property> |             <property name="visible">True</property> | ||||||
|             <property name="can-focus">False</property> |             <property name="can-focus">False</property> | ||||||
|             <property name="revealed">False</property> |             <property name="revealed">False</property> | ||||||
|  |             <child internal-child="action_area"> | ||||||
|  |               <object class="GtkButtonBox"> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">False</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child internal-child="content_area"> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">False</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|             <child> |             <child> | ||||||
|               <placeholder/> |               <placeholder/> | ||||||
|             </child> |             </child> | ||||||
|  | @ -442,7 +462,7 @@ | ||||||
|           <object class="HdyHeaderBar"> |           <object class="HdyHeaderBar"> | ||||||
|             <property name="visible">True</property> |             <property name="visible">True</property> | ||||||
|             <property name="can-focus">False</property> |             <property name="can-focus">False</property> | ||||||
|             <property name="title" translatable="yes">Ensemble</property> |             <property name="title" translatable="yes">Work</property> | ||||||
|           </object> |           </object> | ||||||
|           <packing> |           <packing> | ||||||
|             <property name="expand">False</property> |             <property name="expand">False</property> | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ impl Backend { | ||||||
|         Ok(ensembles) |         Ok(ensembles) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Post a new ensemble to the server and return the ID.
 |     /// Post a new ensemble to the server.
 | ||||||
|     pub async fn post_ensemble(&self, data: &Ensemble) -> Result<()> { |     pub async fn post_ensemble(&self, data: &Ensemble) -> Result<()> { | ||||||
|         self.post("ensembles", serde_json::to_string(data)?).await?; |         self.post("ensembles", serde_json::to_string(data)?).await?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ impl Backend { | ||||||
|         Ok(instruments) |         Ok(instruments) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Post a new instrument to the server and return the ID.
 |     /// Post a new instrument to the server.
 | ||||||
|     pub async fn post_instrument(&self, data: &Instrument) -> Result<()> { |     pub async fn post_instrument(&self, data: &Instrument) -> Result<()> { | ||||||
|         self.post("instruments", serde_json::to_string(data)?).await?; |         self.post("instruments", serde_json::to_string(data)?).await?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|  |  | ||||||
|  | @ -16,6 +16,9 @@ pub use instruments::*; | ||||||
| pub mod persons; | pub mod persons; | ||||||
| pub use persons::*; | pub use persons::*; | ||||||
| 
 | 
 | ||||||
|  | pub mod recordings; | ||||||
|  | pub use recordings::*; | ||||||
|  | 
 | ||||||
| pub mod works; | pub mod works; | ||||||
| pub use works::*; | pub use works::*; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ impl Backend { | ||||||
|         Ok(persons) |         Ok(persons) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Post a new person to the server and return the ID.
 |     /// Post a new person to the server.
 | ||||||
|     pub async fn post_person(&self, data: &Person) -> Result<()> { |     pub async fn post_person(&self, data: &Person) -> Result<()> { | ||||||
|         self.post("persons", serde_json::to_string(data)?).await?; |         self.post("persons", serde_json::to_string(data)?).await?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								musicus/src/backend/client/recordings.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								musicus/src/backend/client/recordings.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | use super::Backend; | ||||||
|  | use crate::database::Recording; | ||||||
|  | use anyhow::Result; | ||||||
|  | 
 | ||||||
|  | impl Backend { | ||||||
|  |     /// Get all available recordings from the server.
 | ||||||
|  |     pub async fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> { | ||||||
|  |         let body = self.get(&format!("works/{}/recordings", work_id)).await?; | ||||||
|  |         let recordings: Vec<Recording> = serde_json::from_str(&body)?; | ||||||
|  |         Ok(recordings) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Post a new recording to the server.
 | ||||||
|  |     pub async fn post_recording(&self, data: &Recording) -> Result<()> { | ||||||
|  |         self.post("recordings", serde_json::to_string(data)?).await?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -10,7 +10,7 @@ impl Backend { | ||||||
|         Ok(works) |         Ok(works) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Post a new work to the server and return the ID.
 |     /// Post a new work to the server.
 | ||||||
|     pub async fn post_work(&self, data: &Work) -> Result<()> { |     pub async fn post_work(&self, data: &Work) -> Result<()> { | ||||||
|         self.post("works", serde_json::to_string(data)?).await?; |         self.post("works", serde_json::to_string(data)?).await?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ use crate::backend::*; | ||||||
| use crate::database::*; | use crate::database::*; | ||||||
| use crate::dialogs::*; | use crate::dialogs::*; | ||||||
| use crate::widgets::*; | use crate::widgets::*; | ||||||
|  | use anyhow::Result; | ||||||
| use gettextrs::gettext; | use gettextrs::gettext; | ||||||
| use glib::clone; | use glib::clone; | ||||||
| use gtk::prelude::*; | use gtk::prelude::*; | ||||||
|  | @ -13,12 +14,14 @@ use std::rc::Rc; | ||||||
| /// A widget for creating or editing a recording.
 | /// A widget for creating or editing a recording.
 | ||||||
| // TODO: Disable buttons if no performance is selected.
 | // TODO: Disable buttons if no performance is selected.
 | ||||||
| pub struct RecordingEditor { | pub struct RecordingEditor { | ||||||
|     pub widget: gtk::Box, |     pub widget: gtk::Stack, | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     parent: gtk::Window, |     parent: gtk::Window, | ||||||
|     save_button: gtk::Button, |     save_button: gtk::Button, | ||||||
|  |     info_bar: gtk::InfoBar, | ||||||
|     work_label: gtk::Label, |     work_label: gtk::Label, | ||||||
|     comment_entry: gtk::Entry, |     comment_entry: gtk::Entry, | ||||||
|  |     upload_switch: gtk::Switch, | ||||||
|     performance_list: Rc<List<Performance>>, |     performance_list: Rc<List<Performance>>, | ||||||
|     id: String, |     id: String, | ||||||
|     work: RefCell<Option<Work>>, |     work: RefCell<Option<Work>>, | ||||||
|  | @ -37,15 +40,16 @@ impl RecordingEditor { | ||||||
|     ) -> Rc<Self> { |     ) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|         let builder = |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui"); | ||||||
|             gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui"); |  | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, gtk::Box, widget); |         get_widget!(builder, gtk::Stack, widget); | ||||||
|         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::InfoBar, info_bar); | ||||||
|         get_widget!(builder, gtk::Button, work_button); |         get_widget!(builder, gtk::Button, work_button); | ||||||
|         get_widget!(builder, gtk::Label, work_label); |         get_widget!(builder, gtk::Label, work_label); | ||||||
|         get_widget!(builder, gtk::Entry, comment_entry); |         get_widget!(builder, gtk::Entry, comment_entry); | ||||||
|  |         get_widget!(builder, gtk::Switch, upload_switch); | ||||||
|         get_widget!(builder, gtk::ScrolledWindow, scroll); |         get_widget!(builder, gtk::ScrolledWindow, scroll); | ||||||
|         get_widget!(builder, gtk::Button, add_performer_button); |         get_widget!(builder, gtk::Button, add_performer_button); | ||||||
|         get_widget!(builder, gtk::Button, edit_performer_button); |         get_widget!(builder, gtk::Button, edit_performer_button); | ||||||
|  | @ -67,8 +71,10 @@ impl RecordingEditor { | ||||||
|             backend, |             backend, | ||||||
|             parent: parent.clone().upcast(), |             parent: parent.clone().upcast(), | ||||||
|             save_button, |             save_button, | ||||||
|  |             info_bar, | ||||||
|             work_label, |             work_label, | ||||||
|             comment_entry, |             comment_entry, | ||||||
|  |             upload_switch, | ||||||
|             performance_list, |             performance_list, | ||||||
|             id, |             id, | ||||||
|             work: RefCell::new(work), |             work: RefCell::new(work), | ||||||
|  | @ -87,20 +93,20 @@ impl RecordingEditor { | ||||||
| 
 | 
 | ||||||
|         this.save_button |         this.save_button | ||||||
|             .connect_clicked(clone!(@strong this => move |_| { |             .connect_clicked(clone!(@strong this => move |_| { | ||||||
|                 let recording = Recording { |                 let context = glib::MainContext::default(); | ||||||
|                     id: this.id.clone(), |  | ||||||
|                     work: this.work.borrow().clone().expect("Tried to create recording without work!"), |  | ||||||
|                     comment: this.comment_entry.get_text().to_string(), |  | ||||||
|                     performances: this.performances.borrow().clone(), |  | ||||||
|                 }; |  | ||||||
|                 
 |  | ||||||
|                 let c = glib::MainContext::default(); |  | ||||||
|                 let clone = this.clone(); |                 let clone = this.clone(); | ||||||
|                 c.spawn_local(async move { |                 context.spawn_local(async move { | ||||||
|                     clone.backend.db().update_recording(recording.clone().into()).await.unwrap(); |                     clone.widget.set_visible_child_name("loading"); | ||||||
|                     if let Some(cb) = &*clone.selected_cb.borrow() { |                     match clone.clone().save().await { | ||||||
|                         cb(recording.clone()); |                         Ok(_) => { | ||||||
|  |                             // We already called the callback.
 | ||||||
|                         } |                         } | ||||||
|  |                         Err(_) => { | ||||||
|  |                             clone.info_bar.set_revealed(true); | ||||||
|  |                             clone.widget.set_visible_child_name("content"); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|                 }); |                 }); | ||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
|  | @ -181,7 +187,8 @@ impl RecordingEditor { | ||||||
|             this.work_selected(work); |             this.work_selected(work); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.performance_list.show_items(this.performances.borrow().clone()); |         this.performance_list | ||||||
|  |             .show_items(this.performances.borrow().clone()); | ||||||
| 
 | 
 | ||||||
|         this |         this | ||||||
|     } |     } | ||||||
|  | @ -198,7 +205,41 @@ impl RecordingEditor { | ||||||
| 
 | 
 | ||||||
|     /// Update the UI according to work.    
 |     /// Update the UI according to work.    
 | ||||||
|     fn work_selected(&self, work: &Work) { |     fn work_selected(&self, work: &Work) { | ||||||
|         self.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title)); |         self.work_label | ||||||
|  |             .set_text(&format!("{}: {}", work.composer.name_fl(), work.title)); | ||||||
|         self.save_button.set_sensitive(true); |         self.save_button.set_sensitive(true); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /// Save the recording and possibly upload it to the server.
 | ||||||
|  |     async fn save(self: Rc<Self>) -> Result<()> { | ||||||
|  |         let recording = Recording { | ||||||
|  |             id: self.id.clone(), | ||||||
|  |             work: self | ||||||
|  |                 .work | ||||||
|  |                 .borrow() | ||||||
|  |                 .clone() | ||||||
|  |                 .expect("Tried to create recording without work!"), | ||||||
|  |             comment: self.comment_entry.get_text().to_string(), | ||||||
|  |             performances: self.performances.borrow().clone(), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let upload = self.upload_switch.get_active(); | ||||||
|  |         if upload { | ||||||
|  |             self.backend.post_recording(&recording).await?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.backend | ||||||
|  |             .db() | ||||||
|  |             .update_recording(recording.clone().into()) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  | 
 | ||||||
|  |         self.backend.library_changed(); | ||||||
|  | 
 | ||||||
|  |         if let Some(cb) = &*self.selected_cb.borrow() { | ||||||
|  |             cb(recording.clone()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ use super::recording_selector_person_screen::*; | ||||||
| use crate::backend::Backend; | use crate::backend::Backend; | ||||||
| use crate::database::*; | use crate::database::*; | ||||||
| use crate::widgets::*; | use crate::widgets::*; | ||||||
|  | use gettextrs::gettext; | ||||||
| use glib::clone; | use glib::clone; | ||||||
| use gtk::prelude::*; | use gtk::prelude::*; | ||||||
| use gtk_macros::get_widget; | use gtk_macros::get_widget; | ||||||
|  | @ -14,6 +15,9 @@ pub struct RecordingSelector { | ||||||
|     pub widget: libhandy::Leaflet, |     pub widget: libhandy::Leaflet, | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|     sidebar_box: gtk::Box, |     sidebar_box: gtk::Box, | ||||||
|  |     server_check_button: gtk::CheckButton, | ||||||
|  |     stack: gtk::Stack, | ||||||
|  |     list: Rc<List<Person>>, | ||||||
|     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, |     selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>, | ||||||
|     add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, |     add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>, | ||||||
|     navigator: Rc<Navigator>, |     navigator: Rc<Navigator>, | ||||||
|  | @ -29,10 +33,15 @@ impl RecordingSelector { | ||||||
|         get_widget!(builder, libhandy::Leaflet, widget); |         get_widget!(builder, libhandy::Leaflet, widget); | ||||||
|         get_widget!(builder, gtk::Button, add_button); |         get_widget!(builder, gtk::Button, add_button); | ||||||
|         get_widget!(builder, gtk::Box, sidebar_box); |         get_widget!(builder, gtk::Box, sidebar_box); | ||||||
|  |         get_widget!(builder, gtk::CheckButton, server_check_button); | ||||||
|  |         get_widget!(builder, gtk::SearchEntry, search_entry); | ||||||
|  |         get_widget!(builder, gtk::Stack, stack); | ||||||
|  |         get_widget!(builder, gtk::ScrolledWindow, scroll); | ||||||
|  |         get_widget!(builder, gtk::Button, try_again_button); | ||||||
|         get_widget!(builder, gtk::Box, empty_screen); |         get_widget!(builder, gtk::Box, empty_screen); | ||||||
| 
 | 
 | ||||||
|         let person_list = PersonList::new(backend.clone()); |         let list = List::<Person>::new(&gettext("No persons found.")); | ||||||
|         sidebar_box.pack_start(&person_list.widget, true, true, 0); |         scroll.add(&list.widget); | ||||||
| 
 | 
 | ||||||
|         let navigator = Navigator::new(&empty_screen); |         let navigator = Navigator::new(&empty_screen); | ||||||
|         widget.add(&navigator.widget); |         widget.add(&navigator.widget); | ||||||
|  | @ -41,6 +50,9 @@ impl RecordingSelector { | ||||||
|             widget, |             widget, | ||||||
|             backend, |             backend, | ||||||
|             sidebar_box, |             sidebar_box, | ||||||
|  |             server_check_button, | ||||||
|  |             stack, | ||||||
|  |             list, | ||||||
|             selected_cb: RefCell::new(None), |             selected_cb: RefCell::new(None), | ||||||
|             add_cb: RefCell::new(None), |             add_cb: RefCell::new(None), | ||||||
|             navigator, |             navigator, | ||||||
|  | @ -54,26 +66,99 @@ impl RecordingSelector { | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         person_list.set_selected(clone!(@strong this => move |person| { |         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_persons().await { | ||||||
|  |                     Ok(persons) => { | ||||||
|  |                         clone.list.show_items(persons); | ||||||
|  |                         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 persons = clone.backend.db().get_persons().await.unwrap(); | ||||||
|  |                 clone.list.show_items(persons); | ||||||
|  |                 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(|person: &Person| { | ||||||
|  |             let label = gtk::Label::new(Some(&person.name_lf())); | ||||||
|  |             label.set_halign(gtk::Align::Start); | ||||||
|  |             label.set_margin_start(6); | ||||||
|  |             label.set_margin_end(6); | ||||||
|  |             label.set_margin_top(6); | ||||||
|  |             label.set_margin_bottom(6); | ||||||
|  |             label.upcast() | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.list | ||||||
|  |             .set_filter(clone!(@strong search_entry => move |person: &Person| { | ||||||
|  |                 let search = search_entry.get_text().to_string().to_lowercase(); | ||||||
|  |                 let name = person.name_fl().to_lowercase(); | ||||||
|  |                 search.is_empty() || name.contains(&search) | ||||||
|  |             })); | ||||||
|  | 
 | ||||||
|  |         this.list | ||||||
|  |             .set_selected(clone!(@strong this => move |person| { | ||||||
|  |                 let online = this.server_check_button.get_active(); | ||||||
|  | 
 | ||||||
|                 let person_screen = RecordingSelectorPersonScreen::new( |                 let person_screen = RecordingSelectorPersonScreen::new( | ||||||
|                     this.backend.clone(), |                     this.backend.clone(), | ||||||
|                     person.clone(), |                     person.clone(), | ||||||
|  |                     online, | ||||||
|                 ); |                 ); | ||||||
| 
 | 
 | ||||||
|             person_screen.set_selected_cb(clone!(@strong this => move |recording| { |                 person_screen.set_selected_cb(clone!(@strong this => move |work| { | ||||||
|                     if let Some(cb) = &*this.selected_cb.borrow() { |                     if let Some(cb) = &*this.selected_cb.borrow() { | ||||||
|                     cb(recording); |                         cb(work); | ||||||
|                     } |                     } | ||||||
|                 })); |                 })); | ||||||
| 
 | 
 | ||||||
|             this.navigator.clone().push(person_screen); |                 this.navigator.clone().replace(person_screen); | ||||||
|                 this.widget.set_visible_child(&this.navigator.widget); |                 this.widget.set_visible_child(&this.navigator.widget); | ||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
|  |         try_again_button.connect_clicked(clone!(@strong load_online => move |_| { | ||||||
|  |             load_online(); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|         this.navigator.set_back_cb(clone!(@strong this => move || { |         this.navigator.set_back_cb(clone!(@strong this => move || { | ||||||
|             this.widget.set_visible_child(&this.sidebar_box); |             this.widget.set_visible_child(&this.sidebar_box); | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|  |         // Initialize
 | ||||||
|  |         load_online(); | ||||||
|  | 
 | ||||||
|         this |         this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,6 +14,8 @@ use std::rc::Rc; | ||||||
| /// screen on selection.
 | /// screen on selection.
 | ||||||
| pub struct RecordingSelectorPersonScreen { | pub struct RecordingSelectorPersonScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|  |     person: Person, | ||||||
|  |     online: bool, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     work_list: Rc<List<Work>>, |     work_list: Rc<List<Work>>, | ||||||
|  | @ -23,7 +25,7 @@ pub struct RecordingSelectorPersonScreen { | ||||||
| 
 | 
 | ||||||
| impl RecordingSelectorPersonScreen { | impl RecordingSelectorPersonScreen { | ||||||
|     /// Create a new recording selector person screen.
 |     /// Create a new recording selector person screen.
 | ||||||
|     pub fn new(backend: Rc<Backend>, person: Person) -> Rc<Self> { |     pub fn new(backend: Rc<Backend>, person: Person, online: bool) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|         let builder = |         let builder = | ||||||
|  | @ -33,14 +35,18 @@ impl RecordingSelectorPersonScreen { | ||||||
|         get_widget!(builder, libhandy::HeaderBar, header); |         get_widget!(builder, libhandy::HeaderBar, header); | ||||||
|         get_widget!(builder, gtk::Button, back_button); |         get_widget!(builder, gtk::Button, back_button); | ||||||
|         get_widget!(builder, gtk::Stack, stack); |         get_widget!(builder, gtk::Stack, stack); | ||||||
|  |         get_widget!(builder, gtk::ScrolledWindow, scroll); | ||||||
|  |         get_widget!(builder, gtk::Button, try_again_button); | ||||||
| 
 | 
 | ||||||
|         header.set_title(Some(&person.name_fl())); |         header.set_title(Some(&person.name_fl())); | ||||||
| 
 | 
 | ||||||
|         let work_list = List::new(&gettext("No works found.")); |         let work_list = List::new(&gettext("No works found.")); | ||||||
|         stack.add_named(&work_list.widget, "content"); |         scroll.add(&work_list.widget); | ||||||
| 
 | 
 | ||||||
|         let this = Rc::new(Self { |         let this = Rc::new(Self { | ||||||
|             backend, |             backend, | ||||||
|  |             person, | ||||||
|  |             online, | ||||||
|             widget, |             widget, | ||||||
|             stack, |             stack, | ||||||
|             work_list, |             work_list, | ||||||
|  | @ -57,6 +63,37 @@ impl RecordingSelectorPersonScreen { | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|  |         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_works(&clone.person.id).await { | ||||||
|  |                     Ok(works) => { | ||||||
|  |                         clone.work_list.show_items(works); | ||||||
|  |                         clone.stack.set_visible_child_name("content"); | ||||||
|  |                     } | ||||||
|  |                     Err(_) => { | ||||||
|  |                         clone.work_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 works = clone.backend.db().get_works(&clone.person.id).await.unwrap(); | ||||||
|  |                 clone.work_list.show_items(works); | ||||||
|  |                 clone.stack.set_visible_child_name("content"); | ||||||
|  |             }); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|         this.work_list.set_make_widget(|work: &Work| { |         this.work_list.set_make_widget(|work: &Work| { | ||||||
|             let label = gtk::Label::new(Some(&work.title)); |             let label = gtk::Label::new(Some(&work.title)); | ||||||
|             label.set_ellipsize(pango::EllipsizeMode::End); |             label.set_ellipsize(pango::EllipsizeMode::End); | ||||||
|  | @ -75,6 +112,7 @@ impl RecordingSelectorPersonScreen { | ||||||
|                     let work_screen = RecordingSelectorWorkScreen::new( |                     let work_screen = RecordingSelectorWorkScreen::new( | ||||||
|                         this.backend.clone(), |                         this.backend.clone(), | ||||||
|                         work.clone(), |                         work.clone(), | ||||||
|  |                         this.online, | ||||||
|                     ); |                     ); | ||||||
| 
 | 
 | ||||||
|                     work_screen.set_selected_cb(clone!(@strong this => move |recording| { |                     work_screen.set_selected_cb(clone!(@strong this => move |recording| { | ||||||
|  | @ -87,16 +125,17 @@ impl RecordingSelectorPersonScreen { | ||||||
|                 } |                 } | ||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
|  |         try_again_button.connect_clicked(clone!(@strong load_online => move |_| { | ||||||
|  |             load_online(); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|         // Initialize
 |         // Initialize
 | ||||||
| 
 | 
 | ||||||
|         let context = glib::MainContext::default(); |         if this.online { | ||||||
|         let clone = this.clone(); |             load_online(); | ||||||
|         context.spawn_local(async move { |         } else { | ||||||
|             let works = clone.backend.db().get_works(&person.id).await.unwrap(); |             load_local(); | ||||||
| 
 |         } | ||||||
|             clone.work_list.show_items(works); |  | ||||||
|             clone.stack.set_visible_child_name("content"); |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         this |         this | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -12,6 +12,8 @@ use std::rc::Rc; | ||||||
| /// A screen within the recording selector presenting a list of recordings for a work.
 | /// A screen within the recording selector presenting a list of recordings for a work.
 | ||||||
| pub struct RecordingSelectorWorkScreen { | pub struct RecordingSelectorWorkScreen { | ||||||
|     backend: Rc<Backend>, |     backend: Rc<Backend>, | ||||||
|  |     work: Work, | ||||||
|  |     online: bool, | ||||||
|     widget: gtk::Box, |     widget: gtk::Box, | ||||||
|     stack: gtk::Stack, |     stack: gtk::Stack, | ||||||
|     recording_list: Rc<List<Recording>>, |     recording_list: Rc<List<Recording>>, | ||||||
|  | @ -21,7 +23,7 @@ pub struct RecordingSelectorWorkScreen { | ||||||
| 
 | 
 | ||||||
| impl RecordingSelectorWorkScreen { | impl RecordingSelectorWorkScreen { | ||||||
|     /// Create a new recording selector work screen.
 |     /// Create a new recording selector work screen.
 | ||||||
|     pub fn new(backend: Rc<Backend>, work: Work) -> Rc<Self> { |     pub fn new(backend: Rc<Backend>, work: Work, online: bool) -> Rc<Self> { | ||||||
|         // Create UI
 |         // Create UI
 | ||||||
| 
 | 
 | ||||||
|         let builder = |         let builder = | ||||||
|  | @ -31,15 +33,19 @@ impl RecordingSelectorWorkScreen { | ||||||
|         get_widget!(builder, libhandy::HeaderBar, header); |         get_widget!(builder, libhandy::HeaderBar, header); | ||||||
|         get_widget!(builder, gtk::Button, back_button); |         get_widget!(builder, gtk::Button, back_button); | ||||||
|         get_widget!(builder, gtk::Stack, stack); |         get_widget!(builder, gtk::Stack, stack); | ||||||
|  |         get_widget!(builder, gtk::ScrolledWindow, scroll); | ||||||
|  |         get_widget!(builder, gtk::Button, try_again_button); | ||||||
| 
 | 
 | ||||||
|         header.set_title(Some(&work.title)); |         header.set_title(Some(&work.title)); | ||||||
|         header.set_subtitle(Some(&work.composer.name_fl())); |         header.set_subtitle(Some(&work.composer.name_fl())); | ||||||
| 
 | 
 | ||||||
|         let recording_list = List::new(&gettext("No recordings found.")); |         let recording_list = List::new(&gettext("No recordings found.")); | ||||||
|         stack.add_named(&recording_list.widget, "content"); |         scroll.add(&recording_list.widget); | ||||||
| 
 | 
 | ||||||
|         let this = Rc::new(Self { |         let this = Rc::new(Self { | ||||||
|             backend, |             backend, | ||||||
|  |             work, | ||||||
|  |             online, | ||||||
|             widget, |             widget, | ||||||
|             stack, |             stack, | ||||||
|             recording_list, |             recording_list, | ||||||
|  | @ -56,6 +62,37 @@ impl RecordingSelectorWorkScreen { | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|  |         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_recordings_for_work(&clone.work.id).await { | ||||||
|  |                     Ok(recordings) => { | ||||||
|  |                         clone.recording_list.show_items(recordings); | ||||||
|  |                         clone.stack.set_visible_child_name("content"); | ||||||
|  |                     } | ||||||
|  |                     Err(_) => { | ||||||
|  |                         clone.recording_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 recordings = clone.backend.db().get_recordings_for_work(&clone.work.id).await.unwrap(); | ||||||
|  |                 clone.recording_list.show_items(recordings); | ||||||
|  |                 clone.stack.set_visible_child_name("content"); | ||||||
|  |             }); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|         this.recording_list |         this.recording_list | ||||||
|             .set_make_widget(|recording: &Recording| { |             .set_make_widget(|recording: &Recording| { | ||||||
|                 let work_label = gtk::Label::new(Some(&recording.work.get_title())); |                 let work_label = gtk::Label::new(Some(&recording.work.get_title())); | ||||||
|  | @ -82,21 +119,17 @@ impl RecordingSelectorWorkScreen { | ||||||
|                 } |                 } | ||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
|  |         try_again_button.connect_clicked(clone!(@strong load_online => move |_| { | ||||||
|  |             load_online(); | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|         // Initialize
 |         // Initialize
 | ||||||
| 
 | 
 | ||||||
|         let context = glib::MainContext::default(); |         if this.online { | ||||||
|         let clone = this.clone(); |             load_online(); | ||||||
|         context.spawn_local(async move { |         } else { | ||||||
|             let recordings = clone |             load_local(); | ||||||
|                 .backend |         } | ||||||
|                 .db() |  | ||||||
|                 .get_recordings_for_work(&work.id) |  | ||||||
|                 .await |  | ||||||
|                 .unwrap(); |  | ||||||
| 
 |  | ||||||
|             clone.recording_list.show_items(recordings); |  | ||||||
|             clone.stack.set_visible_child_name("content"); |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         this |         this | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -152,13 +152,13 @@ impl WorkSelector { | ||||||
|             load_online(); |             load_online(); | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         // Initialize
 |  | ||||||
|         load_online(); |  | ||||||
| 
 |  | ||||||
|         this.navigator.set_back_cb(clone!(@strong this => move || { |         this.navigator.set_back_cb(clone!(@strong this => move || { | ||||||
|             this.widget.set_visible_child(&this.sidebar_box); |             this.widget.set_visible_child(&this.sidebar_box); | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|  |         // Initialize
 | ||||||
|  |         load_online(); | ||||||
|  | 
 | ||||||
|         this |         this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -37,6 +37,7 @@ sources = files( | ||||||
|   'backend/client/ensembles.rs', |   'backend/client/ensembles.rs', | ||||||
|   'backend/client/instruments.rs', |   'backend/client/instruments.rs', | ||||||
|   'backend/client/persons.rs', |   'backend/client/persons.rs', | ||||||
|  |   'backend/client/recordings.rs', | ||||||
|   'backend/client/works.rs', |   'backend/client/works.rs', | ||||||
|   'backend/library.rs', |   'backend/library.rs', | ||||||
|   'backend/mod.rs', |   'backend/mod.rs', | ||||||
|  |  | ||||||
|  | @ -43,6 +43,10 @@ async fn main() -> std::io::Result<()> { | ||||||
|             .service(update_work) |             .service(update_work) | ||||||
|             .service(delete_work) |             .service(delete_work) | ||||||
|             .service(get_works) |             .service(get_works) | ||||||
|  |             .service(get_recording) | ||||||
|  |             .service(update_recording) | ||||||
|  |             .service(delete_recording) | ||||||
|  |             .service(get_recordings_for_work) | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     server.bind("127.0.0.1:8087")?.run().await |     server.bind("127.0.0.1:8087")?.run().await | ||||||
|  |  | ||||||
|  | @ -0,0 +1,74 @@ | ||||||
|  | use super::authenticate; | ||||||
|  | use crate::database; | ||||||
|  | use crate::database::{DbPool, Recording}; | ||||||
|  | use crate::error::ServerError; | ||||||
|  | use actix_web::{delete, get, post, web, HttpResponse}; | ||||||
|  | use actix_web_httpauth::extractors::bearer::BearerAuth; | ||||||
|  | 
 | ||||||
|  | /// Get an existing recording.
 | ||||||
|  | #[get("/recordings/{id}")] | ||||||
|  | pub async fn get_recording( | ||||||
|  |     db: web::Data<DbPool>, | ||||||
|  |     id: web::Path<String>, | ||||||
|  | ) -> Result<HttpResponse, ServerError> { | ||||||
|  |     let data = web::block(move || { | ||||||
|  |         let conn = db.into_inner().get()?; | ||||||
|  |         database::get_recording(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) | ||||||
|  |     }) | ||||||
|  |     .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(HttpResponse::Ok().json(data)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Add a new recording or update an existin one. The user must be authorized to do that.
 | ||||||
|  | #[post("/recordings")] | ||||||
|  | pub async fn update_recording( | ||||||
|  |     auth: BearerAuth, | ||||||
|  |     db: web::Data<DbPool>, | ||||||
|  |     data: web::Json<Recording>, | ||||||
|  | ) -> Result<HttpResponse, ServerError> { | ||||||
|  |     web::block(move || { | ||||||
|  |         let conn = db.into_inner().get()?; | ||||||
|  |         let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; | ||||||
|  | 
 | ||||||
|  |         database::update_recording(&conn, &data.into_inner(), &user)?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     }) | ||||||
|  |     .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(HttpResponse::Ok().finish()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[get("/works/{id}/recordings")] | ||||||
|  | pub async fn get_recordings_for_work( | ||||||
|  |     db: web::Data<DbPool>, | ||||||
|  |     work_id: web::Path<String>, | ||||||
|  | ) -> Result<HttpResponse, ServerError> { | ||||||
|  |     let data = web::block(move || { | ||||||
|  |         let conn = db.into_inner().get()?; | ||||||
|  |         Ok(database::get_recordings_for_work(&conn, &work_id.into_inner())?) | ||||||
|  |     }) | ||||||
|  |     .await?; | ||||||
|  | 
 | ||||||
|  |     Ok(HttpResponse::Ok().json(data)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[delete("/recordings/{id}")] | ||||||
|  | pub async fn delete_recording( | ||||||
|  |     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_recording(&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