From 3e34658b2513a919eea3b4f88023030265047be2 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 8 Nov 2020 02:39:56 +0100 Subject: [PATCH] Tidy up tracks editor and add track editing and deletion --- po/de.po | 38 ++-- po/musicus.pot | 36 ++-- src/dialogs/recording_selector.rs | 58 +++--- src/dialogs/tracks_editor.rs | 334 +++++++++++++++++------------- src/screens/ensemble_screen.rs | 34 +-- src/screens/person_screen.rs | 58 +++--- src/screens/player_screen.rs | 78 ++++--- src/screens/recording_screen.rs | 20 +- src/screens/work_screen.rs | 44 ++-- src/widgets/list.rs | 65 +++--- src/widgets/person_list.rs | 34 +-- src/widgets/poe_list.rs | 24 ++- src/window.rs | 54 ++++- 13 files changed, 503 insertions(+), 374 deletions(-) diff --git a/po/de.po b/po/de.po index 7b0a7c2..e6e7744 100644 --- a/po/de.po +++ b/po/de.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-11-08 00:09+0100\n" -"PO-Revision-Date: 2020-11-08 00:12+0100\n" +"POT-Creation-Date: 2020-11-08 02:37+0100\n" +"PO-Revision-Date: 2020-11-08 02:37+0100\n" "Last-Translator: \n" "Language-Team: \n" "Language: de\n" @@ -53,8 +53,8 @@ msgid "Recordings" msgstr "Aufnahmen" #: res/ui/ensemble_screen.ui:188 res/ui/work_screen.ui:188 -#: src/dialogs/recording_selector.rs:227 src/screens/ensemble_screen.rs:77 -#: src/screens/person_screen.rs:99 src/screens/work_screen.rs:78 +#: src/dialogs/recording_selector.rs:205 src/screens/ensemble_screen.rs:53 +#: src/screens/person_screen.rs:77 src/screens/work_screen.rs:54 msgid "No recordings found." msgstr "Keine Aufnahmen gefunden." @@ -234,7 +234,7 @@ msgid "Select a composer on the left." msgstr "Wählen Sie einen Komponisten aus." #: res/ui/recording_selector.ui:71 res/ui/recording_selector.ui:186 -#: src/dialogs/recording_selector.rs:120 src/screens/person_screen.rs:72 +#: src/dialogs/recording_selector.rs:109 src/screens/person_screen.rs:57 msgid "No works found." msgstr "Keine Werke gefunden." @@ -299,7 +299,7 @@ msgstr "Keine Werkabschnitte hinzugefügt." msgid "Structure" msgstr "Struktur" -#: res/ui/work_selector.ui:113 src/widgets/person_list.rs:41 +#: res/ui/work_selector.ui:113 src/widgets/person_list.rs:26 msgid "No persons found." msgstr "Keine Personen gefunden." @@ -323,19 +323,19 @@ msgstr "Weitere Informationen und Quellcode" msgid "Select music library folder" msgstr "Ordner der Musikbibliothek auswählen" -#: src/dialogs/tracks_editor.rs:59 src/screens/player_screen.rs:232 -#: src/screens/recording_screen.rs:63 -msgid "Unknown" -msgstr "Unbekannt" - -#: src/dialogs/tracks_editor.rs:81 +#: src/dialogs/tracks_editor.rs:60 msgid "Add some tracks." msgstr "Fügen Sie Tracks hinzu." -#: src/dialogs/tracks_editor.rs:148 +#: src/dialogs/tracks_editor.rs:118 msgid "Select audio files" msgstr "Audiodateien auswählen" +#: src/dialogs/tracks_editor.rs:236 src/screens/player_screen.rs:230 +#: src/screens/recording_screen.rs:79 +msgid "Unknown" +msgstr "Unbekannt" + #: src/screens/ensemble_screen.rs:35 msgid "Edit ensemble" msgstr "Ensemble bearbeiten" @@ -360,7 +360,15 @@ msgstr "Aufnahme bearbeiten" msgid "Delete recording" msgstr "Aufnahme löschen" -#: src/screens/recording_screen.rs:85 +#: src/screens/recording_screen.rs:48 +msgid "Edit tracks" +msgstr "Tracks bearbeiten" + +#: src/screens/recording_screen.rs:54 +msgid "Delete tracks" +msgstr "Tracks löschen" + +#: src/screens/recording_screen.rs:69 msgid "No tracks found." msgstr "Keine Tracks gefunden." @@ -372,6 +380,6 @@ msgstr "Werk bearbeiten" msgid "Delete work" msgstr "Werk löschen" -#: src/widgets/poe_list.rs:56 +#: src/widgets/poe_list.rs:41 msgid "No persons or ensembles found." msgstr "Keine Personen oder Ensembles gefunden." diff --git a/po/musicus.pot b/po/musicus.pot index 525ba55..f7d12a4 100644 --- a/po/musicus.pot +++ b/po/musicus.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: musicus\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-11-08 00:09+0100\n" +"POT-Creation-Date: 2020-11-08 02:37+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -52,8 +52,8 @@ msgid "Recordings" msgstr "" #: res/ui/ensemble_screen.ui:188 res/ui/work_screen.ui:188 -#: src/dialogs/recording_selector.rs:227 src/screens/ensemble_screen.rs:77 -#: src/screens/person_screen.rs:99 src/screens/work_screen.rs:78 +#: src/dialogs/recording_selector.rs:205 src/screens/ensemble_screen.rs:53 +#: src/screens/person_screen.rs:77 src/screens/work_screen.rs:54 msgid "No recordings found." msgstr "" @@ -233,7 +233,7 @@ msgid "Select a composer on the left." msgstr "" #: res/ui/recording_selector.ui:71 res/ui/recording_selector.ui:186 -#: src/dialogs/recording_selector.rs:120 src/screens/person_screen.rs:72 +#: src/dialogs/recording_selector.rs:109 src/screens/person_screen.rs:57 msgid "No works found." msgstr "" @@ -293,7 +293,7 @@ msgstr "" msgid "Structure" msgstr "" -#: res/ui/work_selector.ui:113 src/widgets/person_list.rs:41 +#: res/ui/work_selector.ui:113 src/widgets/person_list.rs:26 msgid "No persons found." msgstr "" @@ -317,19 +317,19 @@ msgstr "" msgid "Select music library folder" msgstr "" -#: src/dialogs/tracks_editor.rs:59 src/screens/player_screen.rs:232 -#: src/screens/recording_screen.rs:63 -msgid "Unknown" -msgstr "" - -#: src/dialogs/tracks_editor.rs:81 +#: src/dialogs/tracks_editor.rs:60 msgid "Add some tracks." msgstr "" -#: src/dialogs/tracks_editor.rs:148 +#: src/dialogs/tracks_editor.rs:118 msgid "Select audio files" msgstr "" +#: src/dialogs/tracks_editor.rs:236 src/screens/player_screen.rs:230 +#: src/screens/recording_screen.rs:79 +msgid "Unknown" +msgstr "" + #: src/screens/ensemble_screen.rs:35 msgid "Edit ensemble" msgstr "" @@ -354,7 +354,15 @@ msgstr "" msgid "Delete recording" msgstr "" -#: src/screens/recording_screen.rs:85 +#: src/screens/recording_screen.rs:48 +msgid "Edit tracks" +msgstr "" + +#: src/screens/recording_screen.rs:54 +msgid "Delete tracks" +msgstr "" + +#: src/screens/recording_screen.rs:69 msgid "No tracks found." msgstr "" @@ -366,6 +374,6 @@ msgstr "" msgid "Delete work" msgstr "" -#: src/widgets/poe_list.rs:56 +#: src/widgets/poe_list.rs:41 msgid "No persons or ensembles found." msgstr "" diff --git a/src/dialogs/recording_selector.rs b/src/dialogs/recording_selector.rs index 411764d..c9f28a6 100644 --- a/src/dialogs/recording_selector.rs +++ b/src/dialogs/recording_selector.rs @@ -106,19 +106,17 @@ impl RecordingSelectorPersonScreen { header.set_title(Some(&person.name_fl())); - let work_list = List::new( - |work: &WorkDescription| { - let label = gtk::Label::new(Some(&work.title)); - 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() - }, - |_| true, - &gettext("No works found."), - ); + let work_list = List::new(&gettext("No works found.")); + + work_list.set_make_widget(|work: &WorkDescription| { + let label = gtk::Label::new(Some(&work.title)); + 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() + }); stack.add_named(&work_list.widget, "content"); @@ -204,28 +202,26 @@ impl RecordingSelectorWorkScreen { header.set_title(Some(&work.title)); header.set_subtitle(Some(&work.composer.name_fl())); - let recording_list = List::new( - |recording: &RecordingDescription| { - let work_label = gtk::Label::new(Some(&recording.work.get_title())); + let recording_list = List::new(&gettext("No recordings found.")); - work_label.set_ellipsize(pango::EllipsizeMode::End); - work_label.set_halign(gtk::Align::Start); + recording_list.set_make_widget(|recording: &RecordingDescription| { + let work_label = gtk::Label::new(Some(&recording.work.get_title())); - let performers_label = gtk::Label::new(Some(&recording.get_performers())); - performers_label.set_ellipsize(pango::EllipsizeMode::End); - performers_label.set_opacity(0.5); - performers_label.set_halign(gtk::Align::Start); + work_label.set_ellipsize(pango::EllipsizeMode::End); + work_label.set_halign(gtk::Align::Start); - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&work_label); - vbox.add(&performers_label); + let performers_label = gtk::Label::new(Some(&recording.get_performers())); + performers_label.set_ellipsize(pango::EllipsizeMode::End); + performers_label.set_opacity(0.5); + performers_label.set_halign(gtk::Align::Start); - vbox.upcast() - }, - |_| true, - &gettext("No recordings found."), - ); + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.set_border_width(6); + vbox.add(&work_label); + vbox.add(&performers_label); + + vbox.upcast() + }); stack.add_named(&recording_list.widget, "content"); diff --git a/src/dialogs/tracks_editor.rs b/src/dialogs/tracks_editor.rs index 31f0168..89abe79 100644 --- a/src/dialogs/tracks_editor.rs +++ b/src/dialogs/tracks_editor.rs @@ -9,18 +9,32 @@ use gtk_macros::get_widget; use std::cell::RefCell; use std::rc::Rc; +/// A dialog for editing a set of tracks. +// TODO: Disable buttons if no track is selected. pub struct TracksEditor { + backend: Rc, window: libhandy::Window, + save_button: gtk::Button, + recording_stack: gtk::Stack, + work_label: gtk::Label, + performers_label: gtk::Label, + track_list: Rc>, + recording: RefCell>, + tracks: RefCell>, + callback: RefCell ()>>>, } impl TracksEditor { - pub fn new () + 'static, P: IsA>( + /// Create a new track editor an optionally initialize it with a recording and a list of + /// tracks. + pub fn new>( backend: Rc, parent: &P, recording: Option, tracks: Vec, - callback: F, - ) -> Self { + ) -> Rc { + // UI setup + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui"); get_widget!(builder, libhandy::Window, window); @@ -43,120 +57,82 @@ impl TracksEditor { window.close(); })); - let recording = Rc::new(RefCell::new(recording)); - let tracks = Rc::new(RefCell::new(tracks)); + let track_list = List::new(&gettext("Add some tracks.")); + scroll.add(&track_list.widget); - let track_list = List::new( - clone!(@strong recording => move |track: &TrackDescription| { - let mut title_parts = Vec::::new(); - for part in &track.work_parts { - if let Some(recording) = &*recording.borrow() { - title_parts.push(recording.work.parts[*part].title.clone()); + let this = Rc::new(Self { + backend, + window, + save_button, + recording_stack, + work_label, + performers_label, + track_list, + recording: RefCell::new(recording), + tracks: RefCell::new(tracks), + callback: RefCell::new(None), + }); + + // Signals and callbacks + + this.save_button + .connect_clicked(clone!(@strong this => move |_| { + let context = glib::MainContext::default(); + let this = this.clone(); + context.spawn_local(async move { + this.backend.update_tracks( + this.recording.borrow().as_ref().unwrap().id, + this.tracks.borrow().clone(), + ).await.unwrap(); + + if let Some(callback) = &*this.callback.borrow() { + callback(); } - } - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; + this.window.close(); + }); - let title_label = gtk::Label::new(Some(&title)); - title_label.set_ellipsize(pango::EllipsizeMode::End); - title_label.set_halign(gtk::Align::Start); + })); - let file_name_label = gtk::Label::new(Some(&track.file_name)); - file_name_label.set_ellipsize(pango::EllipsizeMode::End); - file_name_label.set_opacity(0.5); - file_name_label.set_halign(gtk::Align::Start); - - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&title_label); - vbox.add(&file_name_label); - - vbox.upcast() - }), - |_| true, - &gettext("Add some tracks."), - ); - - let autofill_parts = Rc::new(clone!(@strong recording, @strong tracks, @strong track_list => move || { - if let Some(recording) = &*recording.borrow() { - let mut tracks = tracks.borrow_mut(); - for (index, _) in recording.work.parts.iter().enumerate() { - if let Some(mut track) = tracks.get_mut(index) { - track.work_parts = vec!(index); - } else { - break; - } - } - - track_list.show_items(tracks.clone()); - } - })); - - recording_button.connect_clicked(clone!( - @strong backend, - @strong window, - @strong save_button, - @strong work_label, - @strong performers_label, - @strong recording_stack, - @strong recording, - @strong autofill_parts => move |_| { + recording_button.connect_clicked(clone!(@strong this => move |_| { RecordingSelector::new( - backend.clone(), - &window, - clone!( - @strong save_button, - @strong work_label, - @strong performers_label, - @strong recording_stack, - @strong recording, - @strong autofill_parts => move |r| { - work_label.set_text(&r.work.get_title()); - performers_label.set_text(&r.get_performers()); - recording_stack.set_visible_child_name("selected"); - recording.replace(Some(r)); - save_button.set_sensitive(true); - autofill_parts(); - } - )).show(); + this.backend.clone(), + &this.window, + clone!(@strong this => move |recording| { + this.recording_selected(&recording); + this.recording.replace(Some(recording)); + }), + ).show(); } )); - let callback = Rc::new(callback); - save_button.connect_clicked(clone!(@strong window, @strong backend, @strong recording, @strong tracks, @strong callback => move |_| { - let context = glib::MainContext::default(); - let window = window.clone(); - let backend = backend.clone(); - let recording = recording.clone(); - let tracks = tracks.clone(); - let callback = callback.clone(); - context.spawn_local(async move { - backend.update_tracks(recording.borrow().as_ref().unwrap().id, tracks.borrow().clone()).await.unwrap(); - callback(); - window.close(); - }); + this.track_list + .set_make_widget(clone!(@strong this => move |track| { + this.build_track_row(track) + })); - })); + add_track_button.connect_clicked(clone!(@strong this => move |_| { + let music_library_path = this.backend.get_music_library_path().unwrap(); - add_track_button.connect_clicked(clone!(@strong window, @strong tracks, @strong track_list, @strong autofill_parts => move |_| { - let music_library_path = backend.get_music_library_path().unwrap(); + let dialog = gtk::FileChooserNative::new( + Some(&gettext("Select audio files")), + Some(&this.window), + gtk::FileChooserAction::Open, + None, + None, + ); - let dialog = gtk::FileChooserNative::new(Some(&gettext("Select audio files")), Some(&window), gtk::FileChooserAction::Open, None, None); dialog.set_select_multiple(true); dialog.set_current_folder(&music_library_path); if let gtk::ResponseType::Accept = dialog.run() { - let mut index = match track_list.get_selected_index() { + let mut index = match this.track_list.get_selected_index() { Some(index) => index + 1, - None => tracks.borrow().len(), + None => this.tracks.borrow().len(), }; { - let mut tracks = tracks.borrow_mut(); + let mut tracks = this.tracks.borrow_mut(); for file_name in dialog.get_filenames() { let file_name = file_name.strip_prefix(&music_library_path).unwrap(); tracks.insert(index, TrackDescription { @@ -166,75 +142,143 @@ impl TracksEditor { index += 1; } } - - track_list.show_items(tracks.borrow().clone()); - autofill_parts(); - track_list.select_index(index); + + this.track_list.show_items(this.tracks.borrow().clone()); + this.autofill_parts(); + this.track_list.select_index(index); } })); - remove_track_button.connect_clicked( - clone!(@strong tracks, @strong track_list => move |_| { - match track_list.get_selected_index() { - Some(index) => { - tracks.borrow_mut().remove(index); - track_list.show_items(tracks.borrow().clone()); - track_list.select_index(index); - } - None => (), + remove_track_button.connect_clicked(clone!(@strong this => move |_| { + match this.track_list.get_selected_index() { + Some(index) => { + let mut tracks = this.tracks.borrow_mut(); + tracks.remove(index); + this.track_list.show_items(tracks.clone()); + this.track_list.select_index(index); } - }), - ); + None => (), + } + })); - move_track_up_button.connect_clicked( - clone!(@strong tracks, @strong track_list => move |_| { - match track_list.get_selected_index() { - Some(index) => { - if index > 0 { - tracks.borrow_mut().swap(index - 1, index); - track_list.show_items(tracks.borrow().clone()); - track_list.select_index(index - 1); - } + move_track_up_button.connect_clicked(clone!(@strong this => move |_| { + match this.track_list.get_selected_index() { + Some(index) => { + if index > 0 { + let mut tracks = this.tracks.borrow_mut(); + tracks.swap(index - 1, index); + this.track_list.show_items(tracks.clone()); + this.track_list.select_index(index - 1); } - None => (), } - }), - ); + None => (), + } + })); - move_track_down_button.connect_clicked( - clone!(@strong tracks, @strong track_list => move |_| { - match track_list.get_selected_index() { - Some(index) => { - if index < tracks.borrow().len() - 1 { - tracks.borrow_mut().swap(index, index + 1); - track_list.show_items(tracks.borrow().clone()); - track_list.select_index(index + 1); - } + move_track_down_button.connect_clicked(clone!(@strong this => move |_| { + match this.track_list.get_selected_index() { + Some(index) => { + let mut tracks = this.tracks.borrow_mut(); + if index < tracks.len() - 1 { + tracks.swap(index, index + 1); + this.track_list.show_items(tracks.clone()); + this.track_list.select_index(index + 1); } - None => (), } - }), - ); + None => (), + } + })); - edit_track_button.connect_clicked(clone!(@strong window, @strong tracks, @strong track_list, @strong recording => move |_| { - if let Some(index) = track_list.get_selected_index() { - if let Some(recording) = &*recording.borrow() { - TrackEditor::new(&window, tracks.borrow()[index].clone(), recording.work.clone(), clone!(@strong tracks, @strong track_list => move |track| { - let mut tracks = tracks.borrow_mut(); + edit_track_button.connect_clicked(clone!(@strong this => move |_| { + if let Some(index) = this.track_list.get_selected_index() { + if let Some(recording) = &*this.recording.borrow() { + TrackEditor::new(&this.window, this.tracks.borrow()[index].clone(), recording.work.clone(), clone!(@strong this => move |track| { + let mut tracks = this.tracks.borrow_mut(); tracks[index] = track; - track_list.show_items(tracks.clone()); - track_list.select_index(index); + this.track_list.show_items(tracks.clone()); + this.track_list.select_index(index); })).show(); } } })); - scroll.add(&track_list.widget); + // Initialization - Self { window } + if let Some(recording) = &*this.recording.borrow() { + this.recording_selected(recording); + } + + this.track_list.show_items(this.tracks.borrow().clone()); + + this } + /// Set a callback to be called when the tracks are saved. + pub fn set_callback () + 'static>(&self, cb: F) { + self.callback.replace(Some(Box::new(cb))); + } + + /// Open the track editor. pub fn show(&self) { self.window.show(); } + + /// Create a widget representing a track. + fn build_track_row(&self, track: &TrackDescription) -> gtk::Widget { + let mut title_parts = Vec::::new(); + for part in &track.work_parts { + if let Some(recording) = &*self.recording.borrow() { + title_parts.push(recording.work.parts[*part].title.clone()); + } + } + + let title = if title_parts.is_empty() { + gettext("Unknown") + } else { + title_parts.join(", ") + }; + + let title_label = gtk::Label::new(Some(&title)); + title_label.set_ellipsize(pango::EllipsizeMode::End); + title_label.set_halign(gtk::Align::Start); + + let file_name_label = gtk::Label::new(Some(&track.file_name)); + file_name_label.set_ellipsize(pango::EllipsizeMode::End); + file_name_label.set_opacity(0.5); + file_name_label.set_halign(gtk::Align::Start); + + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.set_border_width(6); + vbox.add(&title_label); + vbox.add(&file_name_label); + + vbox.upcast() + } + + /// Set everything up after selecting a recording. + fn recording_selected(&self, recording: &RecordingDescription) { + self.work_label.set_text(&recording.work.get_title()); + self.performers_label.set_text(&recording.get_performers()); + self.recording_stack.set_visible_child_name("selected"); + self.save_button.set_sensitive(true); + self.autofill_parts(); + } + + /// Automatically try to put work part information from the selected recording into the + /// selected tracks. + fn autofill_parts(&self) { + if let Some(recording) = &*self.recording.borrow() { + let mut tracks = self.tracks.borrow_mut(); + + for (index, _) in recording.work.parts.iter().enumerate() { + if let Some(mut track) = tracks.get_mut(index) { + track.work_parts = vec![index]; + } else { + break; + } + } + + self.track_list.show_items(tracks.clone()); + } + } } diff --git a/src/screens/ensemble_screen.rs b/src/screens/ensemble_screen.rs index 3501229..40f8884 100644 --- a/src/screens/ensemble_screen.rs +++ b/src/screens/ensemble_screen.rs @@ -50,31 +50,33 @@ impl EnsembleScreen { menu_button.set_menu_model(Some(&menu)); - let recording_list = List::new( - |recording: &RecordingDescription| { - let work_label = gtk::Label::new(Some(&recording.work.get_title())); + let recording_list = List::new(&gettext("No recordings found.")); - work_label.set_ellipsize(pango::EllipsizeMode::End); - work_label.set_halign(gtk::Align::Start); + recording_list.set_make_widget(|recording: &RecordingDescription| { + let work_label = gtk::Label::new(Some(&recording.work.get_title())); - let performers_label = gtk::Label::new(Some(&recording.get_performers())); - performers_label.set_ellipsize(pango::EllipsizeMode::End); - performers_label.set_opacity(0.5); - performers_label.set_halign(gtk::Align::Start); + work_label.set_ellipsize(pango::EllipsizeMode::End); + work_label.set_halign(gtk::Align::Start); - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&work_label); - vbox.add(&performers_label); + let performers_label = gtk::Label::new(Some(&recording.get_performers())); + performers_label.set_ellipsize(pango::EllipsizeMode::End); + performers_label.set_opacity(0.5); + performers_label.set_halign(gtk::Align::Start); - vbox.upcast() - }, + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.set_border_width(6); + vbox.add(&work_label); + vbox.add(&performers_label); + + vbox.upcast() + }); + + recording_list.set_filter( clone!(@strong search_entry => move |recording: &RecordingDescription| { let search = search_entry.get_text().to_string().to_lowercase(); let text = recording.work.get_title() + &recording.get_performers(); search.is_empty() || text.contains(&search) }), - &gettext("No recordings found."), ); recording_frame.add(&recording_list.widget.clone()); diff --git a/src/screens/person_screen.rs b/src/screens/person_screen.rs index fd8c855..e645f25 100644 --- a/src/screens/person_screen.rs +++ b/src/screens/person_screen.rs @@ -54,49 +54,53 @@ impl PersonScreen { menu_button.set_menu_model(Some(&menu)); - let work_list = List::new( - |work: &WorkDescription| { - let label = gtk::Label::new(Some(&work.title)); - 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() - }, + let work_list = List::new(&gettext("No works found.")); + + work_list.set_make_widget(|work: &WorkDescription| { + let label = gtk::Label::new(Some(&work.title)); + 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() + }); + + work_list.set_filter( clone!(@strong search_entry => move |work: &WorkDescription| { let search = search_entry.get_text().to_string().to_lowercase(); let title = work.title.to_lowercase(); search.is_empty() || title.contains(&search) }), - &gettext("No works found."), ); - let recording_list = List::new( - |recording: &RecordingDescription| { - let work_label = gtk::Label::new(Some(&recording.work.get_title())); + let recording_list = List::new(&gettext("No recordings found.")); - work_label.set_ellipsize(pango::EllipsizeMode::End); - work_label.set_halign(gtk::Align::Start); + recording_list.set_make_widget(|recording: &RecordingDescription| { + let work_label = gtk::Label::new(Some(&recording.work.get_title())); - let performers_label = gtk::Label::new(Some(&recording.get_performers())); - performers_label.set_ellipsize(pango::EllipsizeMode::End); - performers_label.set_opacity(0.5); - performers_label.set_halign(gtk::Align::Start); + work_label.set_ellipsize(pango::EllipsizeMode::End); + work_label.set_halign(gtk::Align::Start); - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&work_label); - vbox.add(&performers_label); + let performers_label = gtk::Label::new(Some(&recording.get_performers())); + performers_label.set_ellipsize(pango::EllipsizeMode::End); + performers_label.set_opacity(0.5); + performers_label.set_halign(gtk::Align::Start); - vbox.upcast() - }, + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.set_border_width(6); + vbox.add(&work_label); + vbox.add(&performers_label); + + vbox.upcast() + }); + + recording_list.set_filter( clone!(@strong search_entry => move |recording: &RecordingDescription| { let search = search_entry.get_text().to_string().to_lowercase(); let text = recording.work.get_title() + &recording.get_performers(); search.is_empty() || text.contains(&search) }), - &gettext("No recordings found."), ); work_frame.add(&work_list.widget); diff --git a/src/screens/player_screen.rs b/src/screens/player_screen.rs index d868716..3264e7d 100644 --- a/src/screens/player_screen.rs +++ b/src/screens/player_screen.rs @@ -123,48 +123,46 @@ impl PlayerScreen { let current_item = Rc::new(Cell::::new(0)); let current_track = Rc::new(Cell::::new(0)); - let list = List::new( - clone!( - @strong current_item, - @strong current_track - => move |element: &PlaylistElement| { - let title_label = gtk::Label::new(Some(&element.title)); - title_label.set_ellipsize(pango::EllipsizeMode::End); - title_label.set_halign(gtk::Align::Start); - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.add(&title_label); - if let Some(subtitle) = &element.subtitle { - let subtitle_label = gtk::Label::new(Some(&subtitle)); - subtitle_label.set_ellipsize(pango::EllipsizeMode::End); - subtitle_label.set_halign(gtk::Align::Start); - subtitle_label.set_opacity(0.5); - vbox.add(&subtitle_label); - } + let list = List::new(""); - let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6); - hbox.set_border_width(6); - - if element.playable { - let image = gtk::Image::new(); - - if element.item == current_item.get() && element.track == current_track.get() { - image.set_from_icon_name( - Some("media-playback-start-symbolic"), - gtk::IconSize::Button, - ); - } - - hbox.add(&image); - } else if element.item > 0 { - hbox.set_margin_top(18); - } - hbox.add(&vbox); - hbox.upcast() + list.set_make_widget(clone!( + @strong current_item, + @strong current_track + => move |element: &PlaylistElement| { + let title_label = gtk::Label::new(Some(&element.title)); + title_label.set_ellipsize(pango::EllipsizeMode::End); + title_label.set_halign(gtk::Align::Start); + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.add(&title_label); + if let Some(subtitle) = &element.subtitle { + let subtitle_label = gtk::Label::new(Some(&subtitle)); + subtitle_label.set_ellipsize(pango::EllipsizeMode::End); + subtitle_label.set_halign(gtk::Align::Start); + subtitle_label.set_opacity(0.5); + vbox.add(&subtitle_label); } - ), - |_| true, - "", - ); + + let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6); + hbox.set_border_width(6); + + if element.playable { + let image = gtk::Image::new(); + + if element.item == current_item.get() && element.track == current_track.get() { + image.set_from_icon_name( + Some("media-playback-start-symbolic"), + gtk::IconSize::Button, + ); + } + + hbox.add(&image); + } else if element.item > 0 { + hbox.set_margin_top(18); + } + hbox.add(&vbox); + hbox.upcast() + } + )); list.set_selected(clone!(@strong player => move |element| { if let Some(player) = &*player.borrow() { diff --git a/src/screens/recording_screen.rs b/src/screens/recording_screen.rs index e73fd7e..92fa5bc 100644 --- a/src/screens/recording_screen.rs +++ b/src/screens/recording_screen.rs @@ -45,14 +45,30 @@ impl RecordingScreen { Some(&glib::Variant::from(recording.id)), ); + let edit_tracks_menu_item = gio::MenuItem::new(Some(&gettext("Edit tracks")), None); + edit_tracks_menu_item.set_action_and_target_value( + Some("win.edit-tracks"), + Some(&glib::Variant::from(recording.id)), + ); + + let delete_tracks_menu_item = gio::MenuItem::new(Some(&gettext("Delete tracks")), None); + delete_tracks_menu_item.set_action_and_target_value( + Some("win.delete-tracks"), + Some(&glib::Variant::from(recording.id)), + ); + let menu = gio::Menu::new(); menu.append_item(&edit_menu_item); menu.append_item(&delete_menu_item); + menu.append_item(&edit_tracks_menu_item); + menu.append_item(&delete_tracks_menu_item); menu_button.set_menu_model(Some(&menu)); let recording = Rc::new(recording); - let list = List::new( + let list = List::new(&gettext("No tracks found.")); + + list.set_make_widget( clone!(@strong recording => move |track: &TrackDescription| { let mut title_parts = Vec::::new(); for part in &track.work_parts { @@ -81,8 +97,6 @@ impl RecordingScreen { vbox.upcast() }), - |_| true, - &gettext("No tracks found."), ); frame.add(&list.widget); diff --git a/src/screens/work_screen.rs b/src/screens/work_screen.rs index e839a0c..f3d5662 100644 --- a/src/screens/work_screen.rs +++ b/src/screens/work_screen.rs @@ -51,32 +51,32 @@ impl WorkScreen { menu_button.set_menu_model(Some(&menu)); - let recording_list = List::new( - |recording: &RecordingDescription| { - let work_label = gtk::Label::new(Some(&recording.work.get_title())); + let recording_list = List::new(&gettext("No recordings found.")); - work_label.set_ellipsize(pango::EllipsizeMode::End); - work_label.set_halign(gtk::Align::Start); + recording_list.set_make_widget(|recording: &RecordingDescription| { + let work_label = gtk::Label::new(Some(&recording.work.get_title())); - let performers_label = gtk::Label::new(Some(&recording.get_performers())); - performers_label.set_ellipsize(pango::EllipsizeMode::End); - performers_label.set_opacity(0.5); - performers_label.set_halign(gtk::Align::Start); + work_label.set_ellipsize(pango::EllipsizeMode::End); + work_label.set_halign(gtk::Align::Start); - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&work_label); - vbox.add(&performers_label); + let performers_label = gtk::Label::new(Some(&recording.get_performers())); + performers_label.set_ellipsize(pango::EllipsizeMode::End); + performers_label.set_opacity(0.5); + performers_label.set_halign(gtk::Align::Start); - vbox.upcast() - }, - clone!(@strong search_entry => move |recording: &RecordingDescription| { - let search = search_entry.get_text().to_string().to_lowercase(); - let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase(); - search.is_empty() || text.contains(&search) - }), - &gettext("No recordings found."), - ); + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.set_border_width(6); + vbox.add(&work_label); + vbox.add(&performers_label); + + vbox.upcast() + }); + + recording_list.set_filter(clone!(@strong search_entry => move |recording: &RecordingDescription| { + let search = search_entry.get_text().to_string().to_lowercase(); + let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase(); + search.is_empty() || text.contains(&search) + }),); recording_frame.add(&recording_list.widget); diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 13fd74d..e781f17 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -11,19 +11,16 @@ where { pub widget: gtk::ListBox, items: RefCell>, - make_widget: Box gtk::Widget + 'static>, - selected: RefCell () + 'static>>>, + make_widget: RefCell gtk::Widget>>>, + filter: RefCell bool>>>, + selected: RefCell ()>>>, } impl List where T: 'static, { - pub fn new(make_widget: M, filter: F, placeholder_text: &str) -> Rc - where - M: Fn(&T) -> gtk::Widget + 'static, - F: Fn(&T) -> bool + 'static, - { + pub fn new(placeholder_text: &str) -> Rc { let placeholder_label = gtk::Label::new(Some(placeholder_text)); placeholder_label.set_margin_top(6); placeholder_label.set_margin_bottom(6); @@ -35,38 +32,46 @@ where widget.set_placeholder(Some(&placeholder_label)); widget.show(); - let result = Rc::new(Self { + let this = Rc::new(Self { widget, items: RefCell::new(Vec::new()), - make_widget: Box::new(make_widget), + make_widget: RefCell::new(None), + filter: RefCell::new(None), selected: RefCell::new(None), }); - result - .widget - .connect_row_activated(clone!(@strong result => move |_, row| { - if let Some(selected) = &*result.selected.borrow() { + this.widget + .connect_row_activated(clone!(@strong this => move |_, row| { + if let Some(selected) = &*this.selected.borrow() { let row = row.get_child().unwrap().downcast::().unwrap(); let index: usize = row.get_index().try_into().unwrap(); - selected(&result.items.borrow()[index]); + selected(&this.items.borrow()[index]); } })); - result - .widget - .set_filter_func(Some(Box::new(clone!(@strong result => move |row| { - let row = row.get_child().unwrap().downcast::().unwrap(); - let index: usize = row.get_index().try_into().unwrap(); - filter(&result.items.borrow()[index]) + this.widget + .set_filter_func(Some(Box::new(clone!(@strong this => move |row| { + if let Some(filter) = &*this.filter.borrow() { + let row = row.get_child().unwrap().downcast::().unwrap(); + let index: usize = row.get_index().try_into().unwrap(); + filter(&this.items.borrow()[index]) + } else { + true + } })))); - result + this } - pub fn set_selected(&self, selected: S) - where - S: Fn(&T) -> () + 'static, - { + pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { + self.make_widget.replace(Some(Box::new(make_widget))); + } + + pub fn set_filter bool + 'static>(&self, filter: F) { + self.filter.replace(Some(Box::new(filter))); + } + + pub fn set_selected () + 'static>(&self, selected: S) { self.selected.replace(Some(Box::new(selected))); } @@ -109,10 +114,12 @@ where self.widget.remove(&child); } - for (index, item) in self.items.borrow().iter().enumerate() { - let row = SelectorRow::new(index.try_into().unwrap(), &(self.make_widget)(item)); - row.show_all(); - self.widget.insert(&row, -1); + if let Some(make_widget) = &*self.make_widget.borrow() { + for (index, item) in self.items.borrow().iter().enumerate() { + let row = SelectorRow::new(index.try_into().unwrap(), &make_widget(item)); + row.show_all(); + self.widget.insert(&row, -1); + } } } diff --git a/src/widgets/person_list.rs b/src/widgets/person_list.rs index df67a3d..17b2aca 100644 --- a/src/widgets/person_list.rs +++ b/src/widgets/person_list.rs @@ -23,23 +23,23 @@ impl PersonList { get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::ScrolledWindow, scrolled_window); - let list = List::new( - |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() - }, - 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) - }), - &gettext("No persons found."), - ); + let list = List::new(&gettext("No persons found.")); + + 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() + }); + + 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) + })); scrolled_window.add(&list.widget); diff --git a/src/widgets/poe_list.rs b/src/widgets/poe_list.rs index 53b72bc..e27b0a5 100644 --- a/src/widgets/poe_list.rs +++ b/src/widgets/poe_list.rs @@ -38,22 +38,24 @@ impl PoeList { get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::ScrolledWindow, scrolled_window); - let list = List::new( - |poe: &PersonOrEnsemble| { - let label = gtk::Label::new(Some(&poe.get_title())); - 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() - }, + let list = List::new(&gettext("No persons or ensembles found.")); + + list.set_make_widget(|poe: &PersonOrEnsemble| { + let label = gtk::Label::new(Some(&poe.get_title())); + 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() + }); + + list.set_filter( clone!(@strong search_entry => move |poe: &PersonOrEnsemble| { let search = search_entry.get_text().to_string().to_lowercase(); let title = poe.get_title().to_lowercase(); search.is_empty() || title.contains(&search) }), - &gettext("No persons or ensembles found."), ); scrolled_window.add(&list.widget); diff --git a/src/window.rs b/src/window.rs index fe95daa..9208c93 100644 --- a/src/window.rs +++ b/src/window.rs @@ -85,9 +85,13 @@ impl Window { })); add_button.connect_clicked(clone!(@strong result => move |_| { - TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new(), clone!(@strong result => move || { + let editor = TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new()); + + editor.set_callback(clone!(@strong result => move || { result.reload(); - })).show(); + })); + + editor.show(); })); result @@ -172,9 +176,13 @@ impl Window { result.window, "add-tracks", clone!(@strong result => move |_, _| { - TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new(), clone!(@strong result => move || { + let editor = TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new()); + + editor.set_callback(clone!(@strong result => move || { result.reload(); - })).show(); + })); + + editor.show(); }) ); @@ -306,6 +314,44 @@ impl Window { }) ); + action!( + result.window, + "edit-tracks", + Some(glib::VariantTy::new("x").unwrap()), + clone!(@strong result => move |_, id| { + let id = id.unwrap().get().unwrap(); + let result = result.clone(); + let c = glib::MainContext::default(); + c.spawn_local(async move { + let recording = result.backend.get_recording_description(id).await.unwrap(); + let tracks = result.backend.get_tracks(id).await.unwrap(); + + let editor = TracksEditor::new(result.backend.clone(), &result.window, Some(recording), tracks); + + editor.set_callback(clone!(@strong result => move || { + result.reload(); + })); + + editor.show(); + }); + }) + ); + + action!( + result.window, + "delete-tracks", + Some(glib::VariantTy::new("x").unwrap()), + clone!(@strong result => move |_, id| { + let id = id.unwrap().get().unwrap(); + let result = result.clone(); + let c = glib::MainContext::default(); + c.spawn_local(async move { + result.backend.delete_tracks(id).await.unwrap(); + result.reload(); + }); + }) + ); + let context = glib::MainContext::default(); let clone = result.clone(); context.spawn_local(async move {