| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  | use super::tracks_editor_track_row::{TrackLocation, TracksEditorTrackData};
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  | use crate::{
 | 
					
						
							| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  |     db::models::{Recording, Track, Work},
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |     editor::{
 | 
					
						
							|  |  |  |         recording_editor::MusicusRecordingEditor,
 | 
					
						
							|  |  |  |         recording_selector_popover::RecordingSelectorPopover,
 | 
					
						
							|  |  |  |         tracks_editor_track_row::TracksEditorTrackRow,
 | 
					
						
							|  |  |  |     },
 | 
					
						
							|  |  |  |     library::MusicusLibrary,
 | 
					
						
							|  |  |  | };
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | use adw::{prelude::*, subclass::prelude::*};
 | 
					
						
							|  |  |  | use gettextrs::gettext;
 | 
					
						
							|  |  |  | use gtk::{
 | 
					
						
							|  |  |  |     gio,
 | 
					
						
							|  |  |  |     glib::{self, clone, subclass::Signal, Properties},
 | 
					
						
							|  |  |  | };
 | 
					
						
							|  |  |  | use once_cell::sync::Lazy;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | use std::{
 | 
					
						
							|  |  |  |     cell::{OnceCell, RefCell},
 | 
					
						
							|  |  |  |     path::PathBuf,
 | 
					
						
							|  |  |  | };
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | mod imp {
 | 
					
						
							|  |  |  |     use super::*;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
 | 
					
						
							|  |  |  |     #[properties(wrapper_type = super::TracksEditor)]
 | 
					
						
							|  |  |  |     #[template(file = "data/ui/tracks_editor.blp")]
 | 
					
						
							|  |  |  |     pub struct TracksEditor {
 | 
					
						
							|  |  |  |         #[property(get, construct_only)]
 | 
					
						
							|  |  |  |         pub navigation: OnceCell<adw::NavigationView>,
 | 
					
						
							|  |  |  |         #[property(get, construct_only)]
 | 
					
						
							|  |  |  |         pub library: OnceCell<MusicusLibrary>,
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         pub recording: RefCell<Option<Recording>>,
 | 
					
						
							|  |  |  |         pub recordings_popover: OnceCell<RecordingSelectorPopover>,
 | 
					
						
							|  |  |  |         pub track_rows: RefCell<Vec<TracksEditorTrackRow>>,
 | 
					
						
							| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  |         pub removed_tracks: RefCell<Vec<Track>>,
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         #[template_child]
 | 
					
						
							|  |  |  |         pub recording_row: TemplateChild<adw::ActionRow>,
 | 
					
						
							|  |  |  |         #[template_child]
 | 
					
						
							|  |  |  |         pub select_recording_box: TemplateChild<gtk::Box>,
 | 
					
						
							|  |  |  |         #[template_child]
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |         pub tracks_label: TemplateChild<gtk::Label>,
 | 
					
						
							|  |  |  |         #[template_child]
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |         pub track_list: TemplateChild<gtk::ListBox>,
 | 
					
						
							|  |  |  |         #[template_child]
 | 
					
						
							|  |  |  |         pub save_row: TemplateChild<adw::ButtonRow>,
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #[glib::object_subclass]
 | 
					
						
							|  |  |  |     impl ObjectSubclass for TracksEditor {
 | 
					
						
							|  |  |  |         const NAME: &'static str = "MusicusTracksEditor";
 | 
					
						
							|  |  |  |         type Type = super::TracksEditor;
 | 
					
						
							|  |  |  |         type ParentType = adw::NavigationPage;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         fn class_init(klass: &mut Self::Class) {
 | 
					
						
							|  |  |  |             klass.bind_template();
 | 
					
						
							|  |  |  |             klass.bind_template_instance_callbacks();
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
 | 
					
						
							|  |  |  |             obj.init_template();
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #[glib::derived_properties]
 | 
					
						
							|  |  |  |     impl ObjectImpl for TracksEditor {
 | 
					
						
							|  |  |  |         fn signals() -> &'static [Signal] {
 | 
					
						
							|  |  |  |             static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
 | 
					
						
							|  |  |  |                 vec![Signal::builder("created")
 | 
					
						
							|  |  |  |                     .param_types([Recording::static_type()])
 | 
					
						
							|  |  |  |                     .build()]
 | 
					
						
							|  |  |  |             });
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             SIGNALS.as_ref()
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         fn constructed(&self) {
 | 
					
						
							|  |  |  |             self.parent_constructed();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             let recordings_popover = RecordingSelectorPopover::new(self.library.get().unwrap());
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             let obj = self.obj().clone();
 | 
					
						
							|  |  |  |             recordings_popover.connect_selected(move |_, recording| {
 | 
					
						
							|  |  |  |                 obj.set_recording(recording);
 | 
					
						
							|  |  |  |             });
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             let obj = self.obj().clone();
 | 
					
						
							|  |  |  |             recordings_popover.connect_create(move |_| {
 | 
					
						
							|  |  |  |                 let editor = MusicusRecordingEditor::new(
 | 
					
						
							|  |  |  |                     obj.imp().navigation.get().unwrap(),
 | 
					
						
							|  |  |  |                     &obj.library(),
 | 
					
						
							|  |  |  |                     None,
 | 
					
						
							|  |  |  |                 );
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 editor.connect_created(clone!(
 | 
					
						
							|  |  |  |                     #[weak]
 | 
					
						
							|  |  |  |                     obj,
 | 
					
						
							|  |  |  |                     move |_, recording| {
 | 
					
						
							|  |  |  |                         obj.set_recording(recording);
 | 
					
						
							|  |  |  |                     }
 | 
					
						
							|  |  |  |                 ));
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 obj.imp().navigation.get().unwrap().push(&editor);
 | 
					
						
							|  |  |  |             });
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.select_recording_box.append(&recordings_popover);
 | 
					
						
							|  |  |  |             self.recordings_popover.set(recordings_popover).unwrap();
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     impl WidgetImpl for TracksEditor {}
 | 
					
						
							|  |  |  |     impl NavigationPageImpl for TracksEditor {}
 | 
					
						
							|  |  |  | }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | glib::wrapper! {
 | 
					
						
							|  |  |  |     pub struct TracksEditor(ObjectSubclass<imp::TracksEditor>)
 | 
					
						
							|  |  |  |         @extends gtk::Widget, adw::NavigationPage;
 | 
					
						
							|  |  |  | }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #[gtk::template_callbacks]
 | 
					
						
							|  |  |  | impl TracksEditor {
 | 
					
						
							|  |  |  |     pub fn new(
 | 
					
						
							|  |  |  |         navigation: &adw::NavigationView,
 | 
					
						
							|  |  |  |         library: &MusicusLibrary,
 | 
					
						
							|  |  |  |         recording: Option<Recording>,
 | 
					
						
							|  |  |  |     ) -> Self {
 | 
					
						
							|  |  |  |         let obj: Self = glib::Object::builder()
 | 
					
						
							|  |  |  |             .property("navigation", navigation)
 | 
					
						
							|  |  |  |             .property("library", library)
 | 
					
						
							|  |  |  |             .build();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if let Some(recording) = recording {
 | 
					
						
							|  |  |  |             obj.imp().save_row.set_title(&gettext("Save changes"));
 | 
					
						
							|  |  |  |             obj.set_recording(recording);
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         obj
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #[template_callback]
 | 
					
						
							|  |  |  |     fn select_recording(&self, _: &adw::ActionRow) {
 | 
					
						
							|  |  |  |         self.imp().recordings_popover.get().unwrap().popup();
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #[template_callback]
 | 
					
						
							|  |  |  |     async fn add_files(&self, _: &adw::ActionRow) {
 | 
					
						
							|  |  |  |         let dialog = gtk::FileDialog::builder()
 | 
					
						
							|  |  |  |             .title(gettext("Select audio files"))
 | 
					
						
							|  |  |  |             .modal(true)
 | 
					
						
							|  |  |  |             .build();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         let root = self.root();
 | 
					
						
							|  |  |  |         let window = root
 | 
					
						
							|  |  |  |             .as_ref()
 | 
					
						
							|  |  |  |             .and_then(|r| r.downcast_ref::<gtk::Window>())
 | 
					
						
							|  |  |  |             .unwrap();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         let obj = self.clone();
 | 
					
						
							|  |  |  |         match dialog.open_multiple_future(Some(window)).await {
 | 
					
						
							|  |  |  |             Err(err) => {
 | 
					
						
							|  |  |  |                 if !err.matches(gtk::DialogError::Dismissed) {
 | 
					
						
							|  |  |  |                     log::error!("File selection failed: {err}");
 | 
					
						
							|  |  |  |                 }
 | 
					
						
							|  |  |  |             }
 | 
					
						
							|  |  |  |             Ok(files) => {
 | 
					
						
							|  |  |  |                 for file in &files {
 | 
					
						
							|  |  |  |                     obj.add_file(
 | 
					
						
							|  |  |  |                         file.unwrap()
 | 
					
						
							|  |  |  |                             .downcast::<gio::File>()
 | 
					
						
							|  |  |  |                             .unwrap()
 | 
					
						
							|  |  |  |                             .path()
 | 
					
						
							|  |  |  |                             .unwrap(),
 | 
					
						
							|  |  |  |                     );
 | 
					
						
							|  |  |  |                 }
 | 
					
						
							|  |  |  |             }
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     fn set_recording(&self, recording: Recording) {
 | 
					
						
							| 
									
										
										
										
											2025-02-16 08:46:40 +01:00
										 |  |  |         self.imp()
 | 
					
						
							|  |  |  |             .recording_row
 | 
					
						
							|  |  |  |             .set_title(&recording.work.to_string());
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |         self.imp()
 | 
					
						
							|  |  |  |             .recording_row
 | 
					
						
							|  |  |  |             .set_subtitle(&recording.performers_string());
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |         // Remove previously added track rows. This is not ideal because the user might be under
 | 
					
						
							|  |  |  |         // the impression that changing the recording will allow to transfer tracks to it. But:
 | 
					
						
							|  |  |  |         // What would happen to the old recording's tracks? What would happen with previously
 | 
					
						
							|  |  |  |         // selected work parts?
 | 
					
						
							|  |  |  |         for track_row in self.imp().track_rows.borrow_mut().drain(..) {
 | 
					
						
							|  |  |  |             self.imp().track_list.remove(&track_row);
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  |         // Forget previously removed tracks (see above).
 | 
					
						
							|  |  |  |         self.imp().removed_tracks.borrow_mut().clear();
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |         let tracks = self
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |             .library()
 | 
					
						
							|  |  |  |             .tracks_for_recording(&recording.recording_id)
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |             .unwrap();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if !tracks.is_empty() {
 | 
					
						
							|  |  |  |             self.imp().save_row.set_title(&gettext("Save changes"));
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for track in tracks {
 | 
					
						
							|  |  |  |                 self.add_track_row(
 | 
					
						
							|  |  |  |                     recording.clone(),
 | 
					
						
							|  |  |  |                     TracksEditorTrackData {
 | 
					
						
							| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  |                         location: TrackLocation::Library(track.clone()),
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |                         parts: track.works,
 | 
					
						
							|  |  |  |                     },
 | 
					
						
							|  |  |  |                 );
 | 
					
						
							|  |  |  |             }
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |         self.imp().tracks_label.set_sensitive(true);
 | 
					
						
							|  |  |  |         self.imp().track_list.set_sensitive(true);
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |         self.imp().recording.replace(Some(recording));
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     fn add_file(&self, path: PathBuf) {
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |         if let Some(recording) = &*self.imp().recording.borrow() {
 | 
					
						
							| 
									
										
										
										
											2025-02-16 08:56:27 +01:00
										 |  |  |             let parts_taken = {
 | 
					
						
							|  |  |  |                 self.imp()
 | 
					
						
							|  |  |  |                     .track_rows
 | 
					
						
							|  |  |  |                     .borrow()
 | 
					
						
							|  |  |  |                     .iter()
 | 
					
						
							|  |  |  |                     .map(|t| t.track_data().parts.clone())
 | 
					
						
							|  |  |  |                     .flatten()
 | 
					
						
							|  |  |  |                     .collect::<Vec<Work>>()
 | 
					
						
							|  |  |  |             };
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             let next_part = recording
 | 
					
						
							|  |  |  |                 .work
 | 
					
						
							|  |  |  |                 .parts
 | 
					
						
							|  |  |  |                 .iter()
 | 
					
						
							|  |  |  |                 .find(|p| !parts_taken.contains(p))
 | 
					
						
							|  |  |  |                 .into_iter()
 | 
					
						
							|  |  |  |                 .cloned()
 | 
					
						
							|  |  |  |                 .collect::<Vec<Work>>();
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |             self.add_track_row(
 | 
					
						
							|  |  |  |                 recording.to_owned(),
 | 
					
						
							|  |  |  |                 TracksEditorTrackData {
 | 
					
						
							| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  |                     location: TrackLocation::System(path),
 | 
					
						
							| 
									
										
										
										
											2025-02-16 08:56:27 +01:00
										 |  |  |                     parts: next_part,
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |                 },
 | 
					
						
							|  |  |  |             );
 | 
					
						
							|  |  |  |         } else {
 | 
					
						
							|  |  |  |             log::warn!("Tried to add track row without recording selected");
 | 
					
						
							|  |  |  |         }
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-15 09:08:20 +01:00
										 |  |  |     fn add_track_row(&self, recording: Recording, track_data: TracksEditorTrackData) {
 | 
					
						
							|  |  |  |         let track_row =
 | 
					
						
							|  |  |  |             TracksEditorTrackRow::new(&self.navigation(), &self.library(), recording, track_data);
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         track_row.connect_remove(clone!(
 | 
					
						
							|  |  |  |             #[weak(rename_to = this)]
 | 
					
						
							|  |  |  |             self,
 | 
					
						
							|  |  |  |             move |row| {
 | 
					
						
							| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  |                 if let TrackLocation::Library(track) = row.track_data().location {
 | 
					
						
							|  |  |  |                     this.imp().removed_tracks.borrow_mut().push(track);
 | 
					
						
							|  |  |  |                 }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  |                 this.imp().track_list.remove(row);
 | 
					
						
							|  |  |  |                 this.imp().track_rows.borrow_mut().retain(|p| p != row);
 | 
					
						
							|  |  |  |             }
 | 
					
						
							|  |  |  |         ));
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.imp()
 | 
					
						
							|  |  |  |             .track_list
 | 
					
						
							|  |  |  |             .insert(&track_row, self.imp().track_rows.borrow().len() as i32);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.imp().track_rows.borrow_mut().push(track_row);
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #[template_callback]
 | 
					
						
							|  |  |  |     fn save(&self) {
 | 
					
						
							| 
									
										
										
										
											2025-02-16 16:30:35 +01:00
										 |  |  |         for track in self.imp().removed_tracks.borrow_mut().drain(..) {
 | 
					
						
							|  |  |  |             self.library().delete_track(&track).unwrap();
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for (index, track_row) in self.imp().track_rows.borrow_mut().drain(..).enumerate() {
 | 
					
						
							|  |  |  |             let track_data = track_row.track_data();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             match track_data.location {
 | 
					
						
							|  |  |  |                 TrackLocation::Undefined => {
 | 
					
						
							|  |  |  |                     log::error!("Failed to save track: Undefined track location.");
 | 
					
						
							|  |  |  |                 }
 | 
					
						
							|  |  |  |                 TrackLocation::Library(track) => self
 | 
					
						
							|  |  |  |                     .library()
 | 
					
						
							|  |  |  |                     .update_track(&track.track_id, index as i32, track_data.parts)
 | 
					
						
							|  |  |  |                     .unwrap(),
 | 
					
						
							|  |  |  |                 TrackLocation::System(path) => {
 | 
					
						
							|  |  |  |                     if let Some(recording) = &*self.imp().recording.borrow() {
 | 
					
						
							|  |  |  |                         self.library()
 | 
					
						
							|  |  |  |                             .import_track(
 | 
					
						
							|  |  |  |                                 &path,
 | 
					
						
							|  |  |  |                                 &recording.recording_id,
 | 
					
						
							|  |  |  |                                 index as i32,
 | 
					
						
							|  |  |  |                                 track_data.parts,
 | 
					
						
							|  |  |  |                             )
 | 
					
						
							|  |  |  |                             .unwrap();
 | 
					
						
							|  |  |  |                     } else {
 | 
					
						
							|  |  |  |                         log::error!("Failed to save track: No recording set.");
 | 
					
						
							|  |  |  |                     }
 | 
					
						
							|  |  |  |                 }
 | 
					
						
							|  |  |  |             }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.imp().track_list.remove(&track_row);
 | 
					
						
							|  |  |  |         }
 | 
					
						
							| 
									
										
										
										
											2025-02-09 10:00:46 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         self.navigation().pop();
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | }
 |