Tidy up tracks editor and add track editing and deletion

This commit is contained in:
Elias Projahn 2020-11-08 02:39:56 +01:00
parent 5002eee67a
commit 3e34658b25
13 changed files with 503 additions and 374 deletions

View file

@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \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: 2020-11-08 00:12+0100\n" "PO-Revision-Date: 2020-11-08 02:37+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: de\n" "Language: de\n"
@ -53,8 +53,8 @@ msgid "Recordings"
msgstr "Aufnahmen" msgstr "Aufnahmen"
#: res/ui/ensemble_screen.ui:188 res/ui/work_screen.ui:188 #: 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/dialogs/recording_selector.rs:205 src/screens/ensemble_screen.rs:53
#: src/screens/person_screen.rs:99 src/screens/work_screen.rs:78 #: src/screens/person_screen.rs:77 src/screens/work_screen.rs:54
msgid "No recordings found." msgid "No recordings found."
msgstr "Keine Aufnahmen gefunden." msgstr "Keine Aufnahmen gefunden."
@ -234,7 +234,7 @@ msgid "Select a composer on the left."
msgstr "Wählen Sie einen Komponisten aus." msgstr "Wählen Sie einen Komponisten aus."
#: res/ui/recording_selector.ui:71 res/ui/recording_selector.ui:186 #: 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." msgid "No works found."
msgstr "Keine Werke gefunden." msgstr "Keine Werke gefunden."
@ -299,7 +299,7 @@ msgstr "Keine Werkabschnitte hinzugefügt."
msgid "Structure" msgid "Structure"
msgstr "Struktur" 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." msgid "No persons found."
msgstr "Keine Personen gefunden." msgstr "Keine Personen gefunden."
@ -323,19 +323,19 @@ msgstr "Weitere Informationen und Quellcode"
msgid "Select music library folder" msgid "Select music library folder"
msgstr "Ordner der Musikbibliothek auswählen" msgstr "Ordner der Musikbibliothek auswählen"
#: src/dialogs/tracks_editor.rs:59 src/screens/player_screen.rs:232 #: src/dialogs/tracks_editor.rs:60
#: src/screens/recording_screen.rs:63
msgid "Unknown"
msgstr "Unbekannt"
#: src/dialogs/tracks_editor.rs:81
msgid "Add some tracks." msgid "Add some tracks."
msgstr "Fügen Sie Tracks hinzu." msgstr "Fügen Sie Tracks hinzu."
#: src/dialogs/tracks_editor.rs:148 #: src/dialogs/tracks_editor.rs:118
msgid "Select audio files" msgid "Select audio files"
msgstr "Audiodateien auswählen" 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 #: src/screens/ensemble_screen.rs:35
msgid "Edit ensemble" msgid "Edit ensemble"
msgstr "Ensemble bearbeiten" msgstr "Ensemble bearbeiten"
@ -360,7 +360,15 @@ msgstr "Aufnahme bearbeiten"
msgid "Delete recording" msgid "Delete recording"
msgstr "Aufnahme löschen" 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." msgid "No tracks found."
msgstr "Keine Tracks gefunden." msgstr "Keine Tracks gefunden."
@ -372,6 +380,6 @@ msgstr "Werk bearbeiten"
msgid "Delete work" msgid "Delete work"
msgstr "Werk löschen" msgstr "Werk löschen"
#: src/widgets/poe_list.rs:56 #: src/widgets/poe_list.rs:41
msgid "No persons or ensembles found." msgid "No persons or ensembles found."
msgstr "Keine Personen oder Ensembles gefunden." msgstr "Keine Personen oder Ensembles gefunden."

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: musicus\n" "Project-Id-Version: musicus\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -52,8 +52,8 @@ msgid "Recordings"
msgstr "" msgstr ""
#: res/ui/ensemble_screen.ui:188 res/ui/work_screen.ui:188 #: 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/dialogs/recording_selector.rs:205 src/screens/ensemble_screen.rs:53
#: src/screens/person_screen.rs:99 src/screens/work_screen.rs:78 #: src/screens/person_screen.rs:77 src/screens/work_screen.rs:54
msgid "No recordings found." msgid "No recordings found."
msgstr "" msgstr ""
@ -233,7 +233,7 @@ msgid "Select a composer on the left."
msgstr "" msgstr ""
#: res/ui/recording_selector.ui:71 res/ui/recording_selector.ui:186 #: 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." msgid "No works found."
msgstr "" msgstr ""
@ -293,7 +293,7 @@ msgstr ""
msgid "Structure" msgid "Structure"
msgstr "" 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." msgid "No persons found."
msgstr "" msgstr ""
@ -317,19 +317,19 @@ msgstr ""
msgid "Select music library folder" msgid "Select music library folder"
msgstr "" msgstr ""
#: src/dialogs/tracks_editor.rs:59 src/screens/player_screen.rs:232 #: src/dialogs/tracks_editor.rs:60
#: src/screens/recording_screen.rs:63
msgid "Unknown"
msgstr ""
#: src/dialogs/tracks_editor.rs:81
msgid "Add some tracks." msgid "Add some tracks."
msgstr "" msgstr ""
#: src/dialogs/tracks_editor.rs:148 #: src/dialogs/tracks_editor.rs:118
msgid "Select audio files" msgid "Select audio files"
msgstr "" 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 #: src/screens/ensemble_screen.rs:35
msgid "Edit ensemble" msgid "Edit ensemble"
msgstr "" msgstr ""
@ -354,7 +354,15 @@ msgstr ""
msgid "Delete recording" msgid "Delete recording"
msgstr "" 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." msgid "No tracks found."
msgstr "" msgstr ""
@ -366,6 +374,6 @@ msgstr ""
msgid "Delete work" msgid "Delete work"
msgstr "" msgstr ""
#: src/widgets/poe_list.rs:56 #: src/widgets/poe_list.rs:41
msgid "No persons or ensembles found." msgid "No persons or ensembles found."
msgstr "" msgstr ""

View file

@ -106,8 +106,9 @@ impl RecordingSelectorPersonScreen {
header.set_title(Some(&person.name_fl())); header.set_title(Some(&person.name_fl()));
let work_list = List::new( let work_list = List::new(&gettext("No works found."));
|work: &WorkDescription| {
work_list.set_make_widget(|work: &WorkDescription| {
let label = gtk::Label::new(Some(&work.title)); let label = gtk::Label::new(Some(&work.title));
label.set_halign(gtk::Align::Start); label.set_halign(gtk::Align::Start);
label.set_margin_start(6); label.set_margin_start(6);
@ -115,10 +116,7 @@ impl RecordingSelectorPersonScreen {
label.set_margin_top(6); label.set_margin_top(6);
label.set_margin_bottom(6); label.set_margin_bottom(6);
label.upcast() label.upcast()
}, });
|_| true,
&gettext("No works found."),
);
stack.add_named(&work_list.widget, "content"); stack.add_named(&work_list.widget, "content");
@ -204,8 +202,9 @@ impl RecordingSelectorWorkScreen {
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( let recording_list = List::new(&gettext("No recordings found."));
|recording: &RecordingDescription| {
recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title())); let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End); work_label.set_ellipsize(pango::EllipsizeMode::End);
@ -222,10 +221,7 @@ impl RecordingSelectorWorkScreen {
vbox.add(&performers_label); vbox.add(&performers_label);
vbox.upcast() vbox.upcast()
}, });
|_| true,
&gettext("No recordings found."),
);
stack.add_named(&recording_list.widget, "content"); stack.add_named(&recording_list.widget, "content");

View file

@ -9,18 +9,32 @@ use gtk_macros::get_widget;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
/// A dialog for editing a set of tracks.
// TODO: Disable buttons if no track is selected.
pub struct TracksEditor { pub struct TracksEditor {
backend: Rc<Backend>,
window: libhandy::Window, window: libhandy::Window,
save_button: gtk::Button,
recording_stack: gtk::Stack,
work_label: gtk::Label,
performers_label: gtk::Label,
track_list: Rc<List<TrackDescription>>,
recording: RefCell<Option<RecordingDescription>>,
tracks: RefCell<Vec<TrackDescription>>,
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
} }
impl TracksEditor { impl TracksEditor {
pub fn new<F: Fn() -> () + 'static, P: IsA<gtk::Window>>( /// Create a new track editor an optionally initialize it with a recording and a list of
/// tracks.
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>, backend: Rc<Backend>,
parent: &P, parent: &P,
recording: Option<RecordingDescription>, recording: Option<RecordingDescription>,
tracks: Vec<TrackDescription>, tracks: Vec<TrackDescription>,
callback: F, ) -> Rc<Self> {
) -> Self { // UI setup
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui"); let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui");
get_widget!(builder, libhandy::Window, window); get_widget!(builder, libhandy::Window, window);
@ -43,14 +57,177 @@ impl TracksEditor {
window.close(); window.close();
})); }));
let recording = Rc::new(RefCell::new(recording)); let track_list = List::new(&gettext("Add some tracks."));
let tracks = Rc::new(RefCell::new(tracks)); scroll.add(&track_list.widget);
let track_list = List::new( let this = Rc::new(Self {
clone!(@strong recording => move |track: &TrackDescription| { 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();
}
this.window.close();
});
}));
recording_button.connect_clicked(clone!(@strong this => move |_| {
RecordingSelector::new(
this.backend.clone(),
&this.window,
clone!(@strong this => move |recording| {
this.recording_selected(&recording);
this.recording.replace(Some(recording));
}),
).show();
}
));
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();
let dialog = gtk::FileChooserNative::new(
Some(&gettext("Select audio files")),
Some(&this.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 this.track_list.get_selected_index() {
Some(index) => index + 1,
None => this.tracks.borrow().len(),
};
{
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 {
work_parts: Vec::new(),
file_name: String::from(file_name.to_str().unwrap()),
});
index += 1;
}
}
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 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 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 => (),
}
}));
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 => (),
}
}));
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;
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index);
})).show();
}
}
}));
// Initialization
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<F: Fn() -> () + '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::<String>::new(); let mut title_parts = Vec::<String>::new();
for part in &track.work_parts { for part in &track.work_parts {
if let Some(recording) = &*recording.borrow() { if let Some(recording) = &*self.recording.borrow() {
title_parts.push(recording.work.parts[*part].title.clone()); title_parts.push(recording.work.parts[*part].title.clone());
} }
} }
@ -76,165 +253,32 @@ impl TracksEditor {
vbox.add(&file_name_label); vbox.add(&file_name_label);
vbox.upcast() vbox.upcast()
}), }
|_| true,
&gettext("Add some tracks."), /// 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();
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() { for (index, _) in recording.work.parts.iter().enumerate() {
if let Some(mut track) = tracks.get_mut(index) { if let Some(mut track) = tracks.get_mut(index) {
track.work_parts = vec!(index); track.work_parts = vec![index];
} else { } else {
break; break;
} }
} }
track_list.show_items(tracks.clone()); self.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 |_| {
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();
}
));
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();
});
}));
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(&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() {
Some(index) => index + 1,
None => tracks.borrow().len(),
};
{
let mut tracks = 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 {
work_parts: Vec::new(),
file_name: String::from(file_name.to_str().unwrap()),
});
index += 1;
}
}
track_list.show_items(tracks.borrow().clone());
autofill_parts();
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 => (),
}
}),
);
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);
}
}
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);
}
}
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();
tracks[index] = track;
track_list.show_items(tracks.clone());
track_list.select_index(index);
})).show();
}
}
}));
scroll.add(&track_list.widget);
Self { window }
}
pub fn show(&self) {
self.window.show();
} }
} }

View file

@ -50,8 +50,9 @@ impl EnsembleScreen {
menu_button.set_menu_model(Some(&menu)); menu_button.set_menu_model(Some(&menu));
let recording_list = List::new( let recording_list = List::new(&gettext("No recordings found."));
|recording: &RecordingDescription| {
recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title())); let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End); work_label.set_ellipsize(pango::EllipsizeMode::End);
@ -68,13 +69,14 @@ impl EnsembleScreen {
vbox.add(&performers_label); vbox.add(&performers_label);
vbox.upcast() vbox.upcast()
}, });
recording_list.set_filter(
clone!(@strong search_entry => move |recording: &RecordingDescription| { clone!(@strong search_entry => move |recording: &RecordingDescription| {
let search = search_entry.get_text().to_string().to_lowercase(); let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title() + &recording.get_performers(); let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search) search.is_empty() || text.contains(&search)
}), }),
&gettext("No recordings found."),
); );
recording_frame.add(&recording_list.widget.clone()); recording_frame.add(&recording_list.widget.clone());

View file

@ -54,8 +54,9 @@ impl PersonScreen {
menu_button.set_menu_model(Some(&menu)); menu_button.set_menu_model(Some(&menu));
let work_list = List::new( let work_list = List::new(&gettext("No works found."));
|work: &WorkDescription| {
work_list.set_make_widget(|work: &WorkDescription| {
let label = gtk::Label::new(Some(&work.title)); let label = gtk::Label::new(Some(&work.title));
label.set_halign(gtk::Align::Start); label.set_halign(gtk::Align::Start);
label.set_margin_start(6); label.set_margin_start(6);
@ -63,17 +64,19 @@ impl PersonScreen {
label.set_margin_top(6); label.set_margin_top(6);
label.set_margin_bottom(6); label.set_margin_bottom(6);
label.upcast() label.upcast()
}, });
work_list.set_filter(
clone!(@strong search_entry => move |work: &WorkDescription| { clone!(@strong search_entry => move |work: &WorkDescription| {
let search = search_entry.get_text().to_string().to_lowercase(); let search = search_entry.get_text().to_string().to_lowercase();
let title = work.title.to_lowercase(); let title = work.title.to_lowercase();
search.is_empty() || title.contains(&search) search.is_empty() || title.contains(&search)
}), }),
&gettext("No works found."),
); );
let recording_list = List::new( let recording_list = List::new(&gettext("No recordings found."));
|recording: &RecordingDescription| {
recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title())); let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End); work_label.set_ellipsize(pango::EllipsizeMode::End);
@ -90,13 +93,14 @@ impl PersonScreen {
vbox.add(&performers_label); vbox.add(&performers_label);
vbox.upcast() vbox.upcast()
}, });
recording_list.set_filter(
clone!(@strong search_entry => move |recording: &RecordingDescription| { clone!(@strong search_entry => move |recording: &RecordingDescription| {
let search = search_entry.get_text().to_string().to_lowercase(); let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title() + &recording.get_performers(); let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search) search.is_empty() || text.contains(&search)
}), }),
&gettext("No recordings found."),
); );
work_frame.add(&work_list.widget); work_frame.add(&work_list.widget);

View file

@ -123,8 +123,9 @@ impl PlayerScreen {
let current_item = Rc::new(Cell::<usize>::new(0)); let current_item = Rc::new(Cell::<usize>::new(0));
let current_track = Rc::new(Cell::<usize>::new(0)); let current_track = Rc::new(Cell::<usize>::new(0));
let list = List::new( let list = List::new("");
clone!(
list.set_make_widget(clone!(
@strong current_item, @strong current_item,
@strong current_track @strong current_track
=> move |element: &PlaylistElement| { => move |element: &PlaylistElement| {
@ -161,10 +162,7 @@ impl PlayerScreen {
hbox.add(&vbox); hbox.add(&vbox);
hbox.upcast() hbox.upcast()
} }
), ));
|_| true,
"",
);
list.set_selected(clone!(@strong player => move |element| { list.set_selected(clone!(@strong player => move |element| {
if let Some(player) = &*player.borrow() { if let Some(player) = &*player.borrow() {

View file

@ -45,14 +45,30 @@ impl RecordingScreen {
Some(&glib::Variant::from(recording.id)), 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(); let menu = gio::Menu::new();
menu.append_item(&edit_menu_item); menu.append_item(&edit_menu_item);
menu.append_item(&delete_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)); menu_button.set_menu_model(Some(&menu));
let recording = Rc::new(recording); 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| { clone!(@strong recording => move |track: &TrackDescription| {
let mut title_parts = Vec::<String>::new(); let mut title_parts = Vec::<String>::new();
for part in &track.work_parts { for part in &track.work_parts {
@ -81,8 +97,6 @@ impl RecordingScreen {
vbox.upcast() vbox.upcast()
}), }),
|_| true,
&gettext("No tracks found."),
); );
frame.add(&list.widget); frame.add(&list.widget);

View file

@ -51,8 +51,9 @@ impl WorkScreen {
menu_button.set_menu_model(Some(&menu)); menu_button.set_menu_model(Some(&menu));
let recording_list = List::new( let recording_list = List::new(&gettext("No recordings found."));
|recording: &RecordingDescription| {
recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title())); let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End); work_label.set_ellipsize(pango::EllipsizeMode::End);
@ -69,14 +70,13 @@ impl WorkScreen {
vbox.add(&performers_label); vbox.add(&performers_label);
vbox.upcast() vbox.upcast()
}, });
clone!(@strong search_entry => move |recording: &RecordingDescription| {
recording_list.set_filter(clone!(@strong search_entry => move |recording: &RecordingDescription| {
let search = search_entry.get_text().to_string().to_lowercase(); let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase(); let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase();
search.is_empty() || text.contains(&search) search.is_empty() || text.contains(&search)
}), }),);
&gettext("No recordings found."),
);
recording_frame.add(&recording_list.widget); recording_frame.add(&recording_list.widget);

View file

@ -11,19 +11,16 @@ where
{ {
pub widget: gtk::ListBox, pub widget: gtk::ListBox,
items: RefCell<Vec<T>>, items: RefCell<Vec<T>>,
make_widget: Box<dyn Fn(&T) -> gtk::Widget + 'static>, make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
selected: RefCell<Option<Box<dyn Fn(&T) -> () + 'static>>>, filter: RefCell<Option<Box<dyn Fn(&T) -> bool>>>,
selected: RefCell<Option<Box<dyn Fn(&T) -> ()>>>,
} }
impl<T> List<T> impl<T> List<T>
where where
T: 'static, T: 'static,
{ {
pub fn new<M, F>(make_widget: M, filter: F, placeholder_text: &str) -> Rc<Self> pub fn new(placeholder_text: &str) -> Rc<Self> {
where
M: Fn(&T) -> gtk::Widget + 'static,
F: Fn(&T) -> bool + 'static,
{
let placeholder_label = gtk::Label::new(Some(placeholder_text)); let placeholder_label = gtk::Label::new(Some(placeholder_text));
placeholder_label.set_margin_top(6); placeholder_label.set_margin_top(6);
placeholder_label.set_margin_bottom(6); placeholder_label.set_margin_bottom(6);
@ -35,38 +32,46 @@ where
widget.set_placeholder(Some(&placeholder_label)); widget.set_placeholder(Some(&placeholder_label));
widget.show(); widget.show();
let result = Rc::new(Self { let this = Rc::new(Self {
widget, widget,
items: RefCell::new(Vec::new()), items: RefCell::new(Vec::new()),
make_widget: Box::new(make_widget), make_widget: RefCell::new(None),
filter: RefCell::new(None),
selected: RefCell::new(None), selected: RefCell::new(None),
}); });
result this.widget
.widget .connect_row_activated(clone!(@strong this => move |_, row| {
.connect_row_activated(clone!(@strong result => move |_, row| { if let Some(selected) = &*this.selected.borrow() {
if let Some(selected) = &*result.selected.borrow() {
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap(); let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap(); let index: usize = row.get_index().try_into().unwrap();
selected(&result.items.borrow()[index]); selected(&this.items.borrow()[index]);
} }
})); }));
result this.widget
.widget .set_filter_func(Some(Box::new(clone!(@strong this => move |row| {
.set_filter_func(Some(Box::new(clone!(@strong result => move |row| { if let Some(filter) = &*this.filter.borrow() {
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap(); let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap(); let index: usize = row.get_index().try_into().unwrap();
filter(&result.items.borrow()[index]) filter(&this.items.borrow()[index])
} else {
true
}
})))); }))));
result this
} }
pub fn set_selected<S>(&self, selected: S) pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
where self.make_widget.replace(Some(Box::new(make_widget)));
S: Fn(&T) -> () + 'static, }
{
pub fn set_filter<F: Fn(&T) -> bool + 'static>(&self, filter: F) {
self.filter.replace(Some(Box::new(filter)));
}
pub fn set_selected<S: Fn(&T) -> () + 'static>(&self, selected: S) {
self.selected.replace(Some(Box::new(selected))); self.selected.replace(Some(Box::new(selected)));
} }
@ -109,12 +114,14 @@ where
self.widget.remove(&child); self.widget.remove(&child);
} }
if let Some(make_widget) = &*self.make_widget.borrow() {
for (index, item) in self.items.borrow().iter().enumerate() { for (index, item) in self.items.borrow().iter().enumerate() {
let row = SelectorRow::new(index.try_into().unwrap(), &(self.make_widget)(item)); let row = SelectorRow::new(index.try_into().unwrap(), &make_widget(item));
row.show_all(); row.show_all();
self.widget.insert(&row, -1); self.widget.insert(&row, -1);
} }
} }
}
pub fn clear_selection(&self) { pub fn clear_selection(&self) {
self.widget.unselect_all(); self.widget.unselect_all();

View file

@ -23,8 +23,9 @@ impl PersonList {
get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::ScrolledWindow, scrolled_window); get_widget!(builder, gtk::ScrolledWindow, scrolled_window);
let list = List::new( let list = List::new(&gettext("No persons found."));
|person: &Person| {
list.set_make_widget(|person: &Person| {
let label = gtk::Label::new(Some(&person.name_lf())); let label = gtk::Label::new(Some(&person.name_lf()));
label.set_halign(gtk::Align::Start); label.set_halign(gtk::Align::Start);
label.set_margin_start(6); label.set_margin_start(6);
@ -32,14 +33,13 @@ impl PersonList {
label.set_margin_top(6); label.set_margin_top(6);
label.set_margin_bottom(6); label.set_margin_bottom(6);
label.upcast() label.upcast()
}, });
clone!(@strong search_entry => move |person: &Person| {
list.set_filter(clone!(@strong search_entry => move |person: &Person| {
let search = search_entry.get_text().to_string().to_lowercase(); let search = search_entry.get_text().to_string().to_lowercase();
let name = person.name_fl().to_lowercase(); let name = person.name_fl().to_lowercase();
search.is_empty() || name.contains(&search) search.is_empty() || name.contains(&search)
}), }));
&gettext("No persons found."),
);
scrolled_window.add(&list.widget); scrolled_window.add(&list.widget);

View file

@ -38,8 +38,9 @@ impl PoeList {
get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::ScrolledWindow, scrolled_window); get_widget!(builder, gtk::ScrolledWindow, scrolled_window);
let list = List::new( let list = List::new(&gettext("No persons or ensembles found."));
|poe: &PersonOrEnsemble| {
list.set_make_widget(|poe: &PersonOrEnsemble| {
let label = gtk::Label::new(Some(&poe.get_title())); let label = gtk::Label::new(Some(&poe.get_title()));
label.set_halign(gtk::Align::Start); label.set_halign(gtk::Align::Start);
label.set_margin_start(6); label.set_margin_start(6);
@ -47,13 +48,14 @@ impl PoeList {
label.set_margin_top(6); label.set_margin_top(6);
label.set_margin_bottom(6); label.set_margin_bottom(6);
label.upcast() label.upcast()
}, });
list.set_filter(
clone!(@strong search_entry => move |poe: &PersonOrEnsemble| { clone!(@strong search_entry => move |poe: &PersonOrEnsemble| {
let search = search_entry.get_text().to_string().to_lowercase(); let search = search_entry.get_text().to_string().to_lowercase();
let title = poe.get_title().to_lowercase(); let title = poe.get_title().to_lowercase();
search.is_empty() || title.contains(&search) search.is_empty() || title.contains(&search)
}), }),
&gettext("No persons or ensembles found."),
); );
scrolled_window.add(&list.widget); scrolled_window.add(&list.widget);

View file

@ -85,9 +85,13 @@ impl Window {
})); }));
add_button.connect_clicked(clone!(@strong result => move |_| { 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(); result.reload();
})).show(); }));
editor.show();
})); }));
result result
@ -172,9 +176,13 @@ impl Window {
result.window, result.window,
"add-tracks", "add-tracks",
clone!(@strong result => move |_, _| { 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(); 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 context = glib::MainContext::default();
let clone = result.clone(); let clone = result.clone();
context.spawn_local(async move { context.spawn_local(async move {