mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Initial port to GTK4
This commit is contained in:
parent
1a9e58d627
commit
801a130ef8
76 changed files with 3098 additions and 6625 deletions
|
|
@ -17,6 +17,5 @@ pub fn show_about_dialog<W: IsA<gtk::Window>>(parent: &W) {
|
|||
.authors(vec![String::from("Elias Projahn <johrpan@gmail.com>")])
|
||||
.build();
|
||||
|
||||
dialog.connect_response(|dialog, _| dialog.close());
|
||||
dialog.show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ impl LoginDialog {
|
|||
this.stack.set_visible_child_name("loading");
|
||||
|
||||
let data = LoginData {
|
||||
username: this.username_entry.get_text().to_string(),
|
||||
password: this.password_entry.get_text().to_string(),
|
||||
username: this.username_entry.get_text().unwrap().to_string(),
|
||||
password: this.password_entry.get_text().unwrap().to_string(),
|
||||
};
|
||||
|
||||
let c = glib::MainContext::default();
|
||||
|
|
|
|||
|
|
@ -43,21 +43,32 @@ impl Preferences {
|
|||
// Connect signals and callbacks
|
||||
|
||||
select_music_library_path_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let dialog = gtk::FileChooserNative::new(
|
||||
let dialog = gtk::FileChooserDialog::new(
|
||||
Some(&gettext("Select music library folder")),
|
||||
Some(&this.window), gtk::FileChooserAction::SelectFolder,None, None);
|
||||
Some(&this.window),
|
||||
gtk::FileChooserAction::SelectFolder,
|
||||
&[
|
||||
(&gettext("Cancel"), gtk::ResponseType::Cancel),
|
||||
(&gettext("Select"), gtk::ResponseType::Accept),
|
||||
]);
|
||||
|
||||
if let gtk::ResponseType::Accept = dialog.run() {
|
||||
if let Some(path) = dialog.get_filename() {
|
||||
this.music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
|
||||
dialog.connect_response(clone!(@strong this => move |dialog, response| {
|
||||
if let gtk::ResponseType::Accept = response {
|
||||
if let Some(file) = dialog.get_file() {
|
||||
if let Some(path) = file.get_path() {
|
||||
this.music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
|
||||
|
||||
let context = glib::MainContext::default();
|
||||
let backend = this.backend.clone();
|
||||
context.spawn_local(async move {
|
||||
backend.set_music_library_path(path).await.unwrap();
|
||||
});
|
||||
let context = glib::MainContext::default();
|
||||
let backend = this.backend.clone();
|
||||
context.spawn_local(async move {
|
||||
backend.set_music_library_path(path).await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
dialog.show();
|
||||
}));
|
||||
|
||||
url_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ impl ServerDialog {
|
|||
}));
|
||||
|
||||
set_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let url = this.url_entry.get_text().to_string();
|
||||
let url = this.url_entry.get_text().unwrap().to_string();
|
||||
this.backend.set_server_url(&url).unwrap();
|
||||
|
||||
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ impl EnsembleEditor {
|
|||
|
||||
/// Save the ensemble and possibly upload it to the server.
|
||||
async fn save(self: Rc<Self>) -> Result<()> {
|
||||
let name = self.name_entry.get_text().to_string();
|
||||
let name = self.name_entry.get_text().unwrap().to_string();
|
||||
|
||||
let ensemble = Ensemble {
|
||||
id: self.id.clone(),
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ impl InstrumentEditor {
|
|||
|
||||
/// Save the instrument and possibly upload it to the server.
|
||||
async fn save(self: Rc<Self>) -> Result<()> {
|
||||
let name = self.name_entry.get_text().to_string();
|
||||
let name = self.name_entry.get_text().unwrap().to_string();
|
||||
|
||||
let instrument = Instrument {
|
||||
id: self.id.clone(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use gettextrs::gettext;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -14,9 +15,9 @@ pub struct PerformanceEditor {
|
|||
backend: Rc<Backend>,
|
||||
widget: gtk::Box,
|
||||
save_button: gtk::Button,
|
||||
person_label: gtk::Label,
|
||||
ensemble_label: gtk::Label,
|
||||
role_label: gtk::Label,
|
||||
person_row: libhandy::ActionRow,
|
||||
ensemble_row: libhandy::ActionRow,
|
||||
role_row: libhandy::ActionRow,
|
||||
reset_role_button: gtk::Button,
|
||||
person: RefCell<Option<Person>>,
|
||||
ensemble: RefCell<Option<Ensemble>>,
|
||||
|
|
@ -39,17 +40,17 @@ impl PerformanceEditor {
|
|||
get_widget!(builder, gtk::Button, ensemble_button);
|
||||
get_widget!(builder, gtk::Button, role_button);
|
||||
get_widget!(builder, gtk::Button, reset_role_button);
|
||||
get_widget!(builder, gtk::Label, person_label);
|
||||
get_widget!(builder, gtk::Label, ensemble_label);
|
||||
get_widget!(builder, gtk::Label, role_label);
|
||||
get_widget!(builder, libhandy::ActionRow, person_row);
|
||||
get_widget!(builder, libhandy::ActionRow, ensemble_row);
|
||||
get_widget!(builder, libhandy::ActionRow, role_row);
|
||||
|
||||
let this = Rc::new(PerformanceEditor {
|
||||
backend,
|
||||
widget,
|
||||
save_button,
|
||||
person_label,
|
||||
ensemble_label,
|
||||
role_label,
|
||||
person_row,
|
||||
ensemble_row,
|
||||
role_row,
|
||||
reset_role_button,
|
||||
person: RefCell::new(None),
|
||||
ensemble: RefCell::new(None),
|
||||
|
|
@ -166,30 +167,36 @@ impl PerformanceEditor {
|
|||
/// Update the UI according to person.
|
||||
fn show_person(&self, person: Option<&Person>) {
|
||||
if let Some(person) = person {
|
||||
self.person_label.set_text(&person.name_fl());
|
||||
self.person_row.set_title(Some(&gettext("Person")));
|
||||
self.person_row.set_subtitle(Some(&person.name_fl()));
|
||||
self.save_button.set_sensitive(true);
|
||||
} else {
|
||||
self.person_label.set_text(&gettext("Select …"));
|
||||
self.person_row.set_title(Some(&gettext("Select a person")));
|
||||
self.person_row.set_subtitle(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the UI according to ensemble.
|
||||
fn show_ensemble(&self, ensemble: Option<&Ensemble>) {
|
||||
if let Some(ensemble) = ensemble {
|
||||
self.ensemble_label.set_text(&ensemble.name);
|
||||
self.ensemble_row.set_title(Some(&gettext("Ensemble")));
|
||||
self.ensemble_row.set_subtitle(Some(&ensemble.name));
|
||||
self.save_button.set_sensitive(true);
|
||||
} else {
|
||||
self.ensemble_label.set_text(&gettext("Select …"));
|
||||
self.ensemble_row.set_title(Some(&gettext("Select an ensemble")));
|
||||
self.ensemble_row.set_subtitle(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the UI according to role.
|
||||
fn show_role(&self, role: Option<&Instrument>) {
|
||||
if let Some(role) = role {
|
||||
self.role_label.set_text(&role.name);
|
||||
self.role_row.set_title(Some(&gettext("Role")));
|
||||
self.role_row.set_subtitle(Some(&role.name));
|
||||
self.reset_role_button.show();
|
||||
} else {
|
||||
self.role_label.set_text(&gettext("Select …"));
|
||||
self.role_row.set_title(Some(&gettext("Select a role")));
|
||||
self.role_row.set_subtitle(None);
|
||||
self.reset_role_button.hide();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ impl PersonEditor {
|
|||
|
||||
/// Save the person and possibly upload it to the server.
|
||||
async fn save(self: Rc<Self>) -> Result<()> {
|
||||
let first_name = self.first_name_entry.get_text().to_string();
|
||||
let last_name = self.last_name_entry.get_text().to_string();
|
||||
let first_name = self.first_name_entry.get_text().unwrap().to_string();
|
||||
let last_name = self.last_name_entry.get_text().unwrap().to_string();
|
||||
|
||||
let person = Person {
|
||||
id: self.id.clone(),
|
||||
|
|
|
|||
|
|
@ -8,20 +8,20 @@ use gettextrs::gettext;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A widget for creating or editing a recording.
|
||||
// TODO: Disable buttons if no performance is selected.
|
||||
pub struct RecordingEditor {
|
||||
pub widget: gtk::Stack,
|
||||
backend: Rc<Backend>,
|
||||
save_button: gtk::Button,
|
||||
info_bar: gtk::InfoBar,
|
||||
work_label: gtk::Label,
|
||||
work_row: libhandy::ActionRow,
|
||||
comment_entry: gtk::Entry,
|
||||
upload_switch: gtk::Switch,
|
||||
performance_list: Rc<List<Performance>>,
|
||||
performance_list: Rc<List>,
|
||||
id: String,
|
||||
work: RefCell<Option<Work>>,
|
||||
performances: RefCell<Vec<Performance>>,
|
||||
|
|
@ -40,17 +40,15 @@ impl RecordingEditor {
|
|||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Button, save_button);
|
||||
get_widget!(builder, gtk::InfoBar, info_bar);
|
||||
get_widget!(builder, libhandy::ActionRow, work_row);
|
||||
get_widget!(builder, gtk::Button, work_button);
|
||||
get_widget!(builder, gtk::Label, work_label);
|
||||
get_widget!(builder, gtk::Entry, comment_entry);
|
||||
get_widget!(builder, gtk::Switch, upload_switch);
|
||||
get_widget!(builder, gtk::ScrolledWindow, scroll);
|
||||
get_widget!(builder, gtk::Frame, performance_frame);
|
||||
get_widget!(builder, gtk::Button, add_performer_button);
|
||||
get_widget!(builder, gtk::Button, edit_performer_button);
|
||||
get_widget!(builder, gtk::Button, remove_performer_button);
|
||||
|
||||
let performance_list = List::new(&gettext("No performers added."));
|
||||
scroll.add(&performance_list.widget);
|
||||
let performance_list = List::new();
|
||||
performance_frame.set_child(Some(&performance_list.widget));
|
||||
|
||||
let (id, work, performances) = match recording {
|
||||
Some(recording) => {
|
||||
|
|
@ -65,7 +63,7 @@ impl RecordingEditor {
|
|||
backend,
|
||||
save_button,
|
||||
info_bar,
|
||||
work_label,
|
||||
work_row,
|
||||
comment_entry,
|
||||
upload_switch,
|
||||
performance_list,
|
||||
|
|
@ -130,16 +128,60 @@ impl RecordingEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
this.performance_list.set_make_widget(|performance| {
|
||||
let label = gtk::Label::new(Some(&performance.get_title()));
|
||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_margin_start(6);
|
||||
label.set_margin_end(6);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
label.upcast()
|
||||
});
|
||||
this.performance_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let performance = &this.performances.borrow()[index];
|
||||
|
||||
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
|
||||
delete_button.set_valign(gtk::Align::Center);
|
||||
|
||||
delete_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let length = {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances.remove(index);
|
||||
performances.len()
|
||||
};
|
||||
|
||||
this.performance_list.update(length);
|
||||
}));
|
||||
|
||||
let edit_button = gtk::Button::from_icon_name(Some("document-edit-symbolic"));
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
|
||||
edit_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
let performance = &this.performances.borrow()[index];
|
||||
|
||||
let editor = PerformanceEditor::new(
|
||||
this.backend.clone(),
|
||||
Some(performance.clone()),
|
||||
);
|
||||
|
||||
editor.set_selected_cb(clone!(@strong this, @strong navigator => move |performance| {
|
||||
let length = {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances[index] = performance;
|
||||
performances.len()
|
||||
};
|
||||
|
||||
this.performance_list.update(length);
|
||||
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
navigator.push(editor);
|
||||
}
|
||||
}));
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&performance.get_title()));
|
||||
row.add_suffix(&delete_button);
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
add_performer_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
|
|
@ -147,16 +189,13 @@ impl RecordingEditor {
|
|||
let editor = PerformanceEditor::new(this.backend.clone(), None);
|
||||
|
||||
editor.set_selected_cb(clone!(@strong this, @strong navigator => move |performance| {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
|
||||
let index = match this.performance_list.get_selected_index() {
|
||||
Some(index) => index + 1,
|
||||
None => performances.len(),
|
||||
let length = {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances.push(performance);
|
||||
performances.len()
|
||||
};
|
||||
|
||||
performances.insert(index, performance);
|
||||
this.performance_list.show_items(performances.clone());
|
||||
this.performance_list.select_index(index);
|
||||
this.performance_list.update(length);
|
||||
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
|
@ -165,47 +204,14 @@ impl RecordingEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
edit_performer_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
if let Some(index) = this.performance_list.get_selected_index() {
|
||||
let performance = &this.performances.borrow()[index];
|
||||
|
||||
let editor = PerformanceEditor::new(
|
||||
this.backend.clone(),
|
||||
Some(performance.clone()),
|
||||
);
|
||||
|
||||
editor.set_selected_cb(clone!(@strong this, @strong navigator => move |performance| {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances[index] = performance;
|
||||
this.performance_list.show_items(performances.clone());
|
||||
this.performance_list.select_index(index);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
navigator.push(editor);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
remove_performer_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(index) = this.performance_list.get_selected_index() {
|
||||
let mut performances = this.performances.borrow_mut();
|
||||
performances.remove(index);
|
||||
this.performance_list.show_items(performances.clone());
|
||||
this.performance_list.select_index(index);
|
||||
}
|
||||
}));
|
||||
|
||||
// Initialize
|
||||
|
||||
if let Some(work) = &*this.work.borrow() {
|
||||
this.work_selected(work);
|
||||
}
|
||||
|
||||
this.performance_list
|
||||
.show_items(this.performances.borrow().clone());
|
||||
let length = this.performances.borrow().len();
|
||||
this.performance_list.update(length);
|
||||
|
||||
this
|
||||
}
|
||||
|
|
@ -217,8 +223,8 @@ impl RecordingEditor {
|
|||
|
||||
/// Update the UI according to work.
|
||||
fn work_selected(&self, work: &Work) {
|
||||
self.work_label
|
||||
.set_text(&format!("{}: {}", work.composer.name_fl(), work.title));
|
||||
self.work_row.set_title(Some(&gettext("Work")));
|
||||
self.work_row.set_subtitle(Some(&work.get_title()));
|
||||
self.save_button.set_sensitive(true);
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +237,7 @@ impl RecordingEditor {
|
|||
.borrow()
|
||||
.clone()
|
||||
.expect("Tried to create recording without work!"),
|
||||
comment: self.comment_entry.get_text().to_string(),
|
||||
comment: self.comment_entry.get_text().unwrap().to_string(),
|
||||
performances: self.performances.borrow().clone(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use gettextrs::gettext;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::convert::TryInto;
|
||||
use std::rc::Rc;
|
||||
|
|
@ -20,6 +21,15 @@ enum PartOrSection {
|
|||
Section(WorkSection),
|
||||
}
|
||||
|
||||
impl PartOrSection {
|
||||
pub fn get_title(&self) -> String {
|
||||
match self {
|
||||
PartOrSection::Part(part) => part.title.clone(),
|
||||
PartOrSection::Section(section) => section.title.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget for editing and creating works.
|
||||
pub struct WorkEditor {
|
||||
widget: gtk::Stack,
|
||||
|
|
@ -27,10 +37,10 @@ pub struct WorkEditor {
|
|||
save_button: gtk::Button,
|
||||
title_entry: gtk::Entry,
|
||||
info_bar: gtk::InfoBar,
|
||||
composer_label: gtk::Label,
|
||||
composer_row: libhandy::ActionRow,
|
||||
upload_switch: gtk::Switch,
|
||||
instrument_list: Rc<List<Instrument>>,
|
||||
part_list: Rc<List<PartOrSection>>,
|
||||
instrument_list: Rc<List>,
|
||||
part_list: Rc<List>,
|
||||
id: String,
|
||||
composer: RefCell<Option<Person>>,
|
||||
instruments: RefCell<Vec<Instrument>>,
|
||||
|
|
@ -52,24 +62,19 @@ impl WorkEditor {
|
|||
get_widget!(builder, gtk::InfoBar, info_bar);
|
||||
get_widget!(builder, gtk::Entry, title_entry);
|
||||
get_widget!(builder, gtk::Button, composer_button);
|
||||
get_widget!(builder, gtk::Label, composer_label);
|
||||
get_widget!(builder, libhandy::ActionRow, composer_row);
|
||||
get_widget!(builder, gtk::Switch, upload_switch);
|
||||
get_widget!(builder, gtk::ScrolledWindow, instruments_scroll);
|
||||
get_widget!(builder, gtk::Frame, instrument_frame);
|
||||
get_widget!(builder, gtk::Button, add_instrument_button);
|
||||
get_widget!(builder, gtk::Button, remove_instrument_button);
|
||||
get_widget!(builder, gtk::ScrolledWindow, structure_scroll);
|
||||
get_widget!(builder, gtk::Frame, structure_frame);
|
||||
get_widget!(builder, gtk::Button, add_part_button);
|
||||
get_widget!(builder, gtk::Button, remove_part_button);
|
||||
get_widget!(builder, gtk::Button, add_section_button);
|
||||
get_widget!(builder, gtk::Button, edit_part_button);
|
||||
get_widget!(builder, gtk::Button, move_part_up_button);
|
||||
get_widget!(builder, gtk::Button, move_part_down_button);
|
||||
|
||||
let instrument_list = List::new(&gettext("No instruments added."));
|
||||
instruments_scroll.add(&instrument_list.widget);
|
||||
let instrument_list = List::new();
|
||||
instrument_frame.set_child(Some(&instrument_list.widget));
|
||||
|
||||
let part_list = List::new(&gettext("No work parts added."));
|
||||
structure_scroll.add(&part_list.widget);
|
||||
let part_list = List::new();
|
||||
structure_frame.set_child(Some(&part_list.widget));
|
||||
|
||||
let (id, composer, instruments, structure) = match work {
|
||||
Some(work) => {
|
||||
|
|
@ -100,7 +105,7 @@ impl WorkEditor {
|
|||
id,
|
||||
info_bar,
|
||||
title_entry,
|
||||
composer_label,
|
||||
composer_row,
|
||||
upload_switch,
|
||||
instrument_list,
|
||||
part_list,
|
||||
|
|
@ -157,16 +162,28 @@ impl WorkEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
this.instrument_list.set_make_widget(|instrument| {
|
||||
let label = gtk::Label::new(Some(&instrument.name));
|
||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_margin_start(6);
|
||||
label.set_margin_end(6);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
label.upcast()
|
||||
});
|
||||
this.instrument_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let instrument = &this.instruments.borrow()[index];
|
||||
|
||||
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
|
||||
delete_button.set_valign(gtk::Align::Center);
|
||||
|
||||
delete_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let length = {
|
||||
let mut instruments = this.instruments.borrow_mut();
|
||||
instruments.remove(index);
|
||||
instruments.len()
|
||||
};
|
||||
|
||||
this.instrument_list.update(length);
|
||||
}));
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_title(Some(&instrument.name));
|
||||
row.add_suffix(&delete_button);
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
add_instrument_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
|
|
@ -174,17 +191,13 @@ impl WorkEditor {
|
|||
let selector = InstrumentSelector::new(this.backend.clone());
|
||||
|
||||
selector.set_selected_cb(clone!(@strong this, @strong navigator => move |instrument| {
|
||||
let mut instruments = this.instruments.borrow_mut();
|
||||
|
||||
let index = match this.instrument_list.get_selected_index() {
|
||||
Some(index) => index + 1,
|
||||
None => instruments.len(),
|
||||
let length = {
|
||||
let mut instruments = this.instruments.borrow_mut();
|
||||
instruments.push(instrument.clone());
|
||||
instruments.len()
|
||||
};
|
||||
|
||||
instruments.insert(index, instrument.clone());
|
||||
this.instrument_list.show_items(instruments.clone());
|
||||
this.instrument_list.select_index(index);
|
||||
|
||||
this.instrument_list.update(length);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
|
|
@ -192,57 +205,107 @@ impl WorkEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
remove_instrument_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(index) = this.instrument_list.get_selected_index() {
|
||||
let mut instruments = this.instruments.borrow_mut();
|
||||
instruments.remove(index);
|
||||
this.instrument_list.show_items(instruments.clone());
|
||||
this.instrument_list.select_index(index);
|
||||
this.part_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let pos = &this.structure.borrow()[index];
|
||||
|
||||
let drag_source = gtk::DragSource::new();
|
||||
drag_source.set_content(Some(&gdk::ContentProvider::new_for_value(&(index as u32).to_value())));
|
||||
|
||||
let drop_target = gtk::DropTarget::new(glib::Type::U32, gdk::DragAction::MOVE);
|
||||
|
||||
drop_target.connect_property_value_notify(clone!(@strong this => move |drop_target| {
|
||||
println!("{:?} -> {:?}", drop_target.get_value(), index);
|
||||
}));
|
||||
|
||||
let handle = gtk::Image::from_icon_name(Some("open-menu-symbolic"));
|
||||
handle.add_controller(&drag_source);
|
||||
|
||||
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
|
||||
delete_button.set_valign(gtk::Align::Center);
|
||||
|
||||
delete_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.remove(index);
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
}));
|
||||
|
||||
let edit_button = gtk::Button::from_icon_name(Some("document-edit-symbolic"));
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
|
||||
edit_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
match this.structure.borrow()[index].clone() {
|
||||
PartOrSection::Part(part) => {
|
||||
let editor = WorkPartEditor::new(this.backend.clone(), Some(part));
|
||||
|
||||
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |part| {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure[index] = PartOrSection::Part(part);
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
navigator.push(editor);
|
||||
}
|
||||
PartOrSection::Section(section) => {
|
||||
let editor = WorkSectionEditor::new(Some(section));
|
||||
|
||||
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |section| {
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure[index] = PartOrSection::Section(section);
|
||||
structure.len()
|
||||
};
|
||||
|
||||
this.part_list.update(length);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
navigator.push(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&pos.get_title()));
|
||||
row.add_prefix(&handle);
|
||||
row.add_suffix(&delete_button);
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
row.add_controller(&drop_target);
|
||||
|
||||
if let PartOrSection::Part(_) = pos {
|
||||
// TODO: Replace with better solution to differentiate parts and sections.
|
||||
row.set_margin_start(12);
|
||||
}
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.part_list.set_make_widget(|pos| {
|
||||
let label = gtk::Label::new(None);
|
||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_margin_end(6);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
|
||||
match pos {
|
||||
PartOrSection::Part(part) => {
|
||||
label.set_text(&part.title);
|
||||
label.set_margin_start(12);
|
||||
}
|
||||
PartOrSection::Section(section) => {
|
||||
let attrs = pango::AttrList::new();
|
||||
attrs.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap());
|
||||
label.set_attributes(Some(&attrs));
|
||||
label.set_text(§ion.title);
|
||||
label.set_margin_start(6);
|
||||
}
|
||||
}
|
||||
|
||||
label.upcast()
|
||||
});
|
||||
|
||||
add_part_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
let editor = WorkPartEditor::new(this.backend.clone(), None);
|
||||
|
||||
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |part| {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
|
||||
let index = match this.part_list.get_selected_index() {
|
||||
Some(index) => index + 1,
|
||||
None => structure.len(),
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.push(PartOrSection::Part(part));
|
||||
structure.len()
|
||||
};
|
||||
|
||||
structure.insert(index, PartOrSection::Part(part));
|
||||
this.part_list.show_items(structure.clone());
|
||||
this.part_list.select_index(index);
|
||||
|
||||
this.part_list.update(length);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
|
|
@ -256,17 +319,13 @@ impl WorkEditor {
|
|||
let editor = WorkSectionEditor::new(None);
|
||||
|
||||
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |section| {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
|
||||
let index = match this.part_list.get_selected_index() {
|
||||
Some(index) => index + 1,
|
||||
None => structure.len(),
|
||||
let length = {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.push(PartOrSection::Section(section));
|
||||
structure.len()
|
||||
};
|
||||
|
||||
structure.insert(index, PartOrSection::Section(section));
|
||||
this.part_list.show_items(structure.clone());
|
||||
this.part_list.select_index(index);
|
||||
|
||||
this.part_list.update(length);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
|
|
@ -274,82 +333,14 @@ impl WorkEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
edit_part_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
if let Some(index) = this.part_list.get_selected_index() {
|
||||
match this.structure.borrow()[index].clone() {
|
||||
PartOrSection::Part(part) => {
|
||||
let editor = WorkPartEditor::new(this.backend.clone(), Some(part));
|
||||
|
||||
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |part| {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure[index] = PartOrSection::Part(part);
|
||||
this.part_list.show_items(structure.clone());
|
||||
this.part_list.select_index(index);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
navigator.push(editor);
|
||||
}
|
||||
PartOrSection::Section(section) => {
|
||||
let editor = WorkSectionEditor::new(Some(section));
|
||||
|
||||
editor.set_ready_cb(clone!(@strong this, @strong navigator => move |section| {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure[index] = PartOrSection::Section(section);
|
||||
this.part_list.show_items(structure.clone());
|
||||
this.part_list.select_index(index);
|
||||
navigator.clone().pop();
|
||||
}));
|
||||
|
||||
navigator.push(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
remove_part_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(index) = this.part_list.get_selected_index() {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.remove(index);
|
||||
this.part_list.show_items(structure.clone());
|
||||
this.part_list.select_index(index);
|
||||
}
|
||||
}));
|
||||
|
||||
move_part_up_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(index) = this.part_list.get_selected_index() {
|
||||
if index > 0 {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
structure.swap(index - 1, index);
|
||||
this.part_list.show_items(structure.clone());
|
||||
this.part_list.select_index(index - 1);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
move_part_down_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(index) = this.part_list.get_selected_index() {
|
||||
let mut structure = this.structure.borrow_mut();
|
||||
if index < structure.len() - 1 {
|
||||
structure.swap(index, index + 1);
|
||||
this.part_list.show_items(structure.clone());
|
||||
this.part_list.select_index(index + 1);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Initialization
|
||||
|
||||
if let Some(composer) = &*this.composer.borrow() {
|
||||
this.show_composer(composer);
|
||||
}
|
||||
|
||||
this.instrument_list
|
||||
.show_items(this.instruments.borrow().clone());
|
||||
this.part_list.show_items(this.structure.borrow().clone());
|
||||
this.instrument_list.update(this.instruments.borrow().len());
|
||||
this.part_list.update(this.structure.borrow().len());
|
||||
|
||||
this
|
||||
}
|
||||
|
|
@ -361,7 +352,8 @@ impl WorkEditor {
|
|||
|
||||
/// Update the UI according to person.
|
||||
fn show_composer(&self, person: &Person) {
|
||||
self.composer_label.set_text(&person.name_fl());
|
||||
self.composer_row.set_title(Some(&gettext("Composer")));
|
||||
self.composer_row.set_subtitle(Some(&person.name_fl()));
|
||||
self.save_button.set_sensitive(true);
|
||||
}
|
||||
|
||||
|
|
@ -385,7 +377,7 @@ impl WorkEditor {
|
|||
|
||||
let work = Work {
|
||||
id: self.id.clone(),
|
||||
title: self.title_entry.get_text().to_string(),
|
||||
title: self.title_entry.get_text().unwrap().to_string(),
|
||||
composer: self
|
||||
.composer
|
||||
.borrow()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use gettextrs::gettext;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ pub struct WorkPartEditor {
|
|||
backend: Rc<Backend>,
|
||||
widget: gtk::Box,
|
||||
title_entry: gtk::Entry,
|
||||
composer_label: gtk::Label,
|
||||
composer_row: libhandy::ActionRow,
|
||||
reset_composer_button: gtk::Button,
|
||||
composer: RefCell<Option<Person>>,
|
||||
ready_cb: RefCell<Option<Box<dyn Fn(WorkPart) -> ()>>>,
|
||||
|
|
@ -33,7 +34,7 @@ impl WorkPartEditor {
|
|||
get_widget!(builder, gtk::Button, save_button);
|
||||
get_widget!(builder, gtk::Entry, title_entry);
|
||||
get_widget!(builder, gtk::Button, composer_button);
|
||||
get_widget!(builder, gtk::Label, composer_label);
|
||||
get_widget!(builder, libhandy::ActionRow, composer_row);
|
||||
get_widget!(builder, gtk::Button, reset_composer_button);
|
||||
|
||||
let composer = match part {
|
||||
|
|
@ -48,7 +49,7 @@ impl WorkPartEditor {
|
|||
backend,
|
||||
widget,
|
||||
title_entry,
|
||||
composer_label,
|
||||
composer_row,
|
||||
reset_composer_button,
|
||||
composer: RefCell::new(composer),
|
||||
ready_cb: RefCell::new(None),
|
||||
|
|
@ -67,7 +68,7 @@ impl WorkPartEditor {
|
|||
save_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(cb) = &*this.ready_cb.borrow() {
|
||||
cb(WorkPart {
|
||||
title: this.title_entry.get_text().to_string(),
|
||||
title: this.title_entry.get_text().unwrap().to_string(),
|
||||
composer: this.composer.borrow().clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -117,10 +118,12 @@ impl WorkPartEditor {
|
|||
/// Update the UI according to person.
|
||||
fn show_composer(&self, person: Option<&Person>) {
|
||||
if let Some(person) = person {
|
||||
self.composer_label.set_text(&person.name_fl());
|
||||
self.composer_row.set_title(Some(&gettext("Composer")));
|
||||
self.composer_row.set_subtitle(Some(&person.name_fl()));
|
||||
self.reset_composer_button.show();
|
||||
} else {
|
||||
self.composer_label.set_text(&gettext("Select …"));
|
||||
self.composer_row.set_title(Some(&gettext("Select a composer")));
|
||||
self.composer_row.set_subtitle(None);
|
||||
self.reset_composer_button.hide();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ impl WorkSectionEditor {
|
|||
if let Some(cb) = &*this.ready_cb.borrow() {
|
||||
cb(WorkSection {
|
||||
before_index: 0,
|
||||
title: this.title_entry.get_text().to_string(),
|
||||
title: this.title_entry.get_text().unwrap().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ use discid::DiscId;
|
|||
use futures_channel::oneshot;
|
||||
use gstreamer::prelude::*;
|
||||
use gstreamer::{Element, ElementFactory, Pipeline};
|
||||
use std::cell::RefCell;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ use super::disc_source::DiscSource;
|
|||
use super::track_set_editor::{TrackSetData, TrackSetEditor};
|
||||
use crate::database::{generate_id, Medium, Track, TrackSet};
|
||||
use crate::backend::Backend;
|
||||
use crate::widgets::{Navigator, NavigatorScreen};
|
||||
use crate::widgets::new_list::List;
|
||||
use crate::widgets::{List, Navigator, NavigatorScreen};
|
||||
use anyhow::Result;
|
||||
use glib::clone;
|
||||
use glib::prelude::*;
|
||||
|
|
@ -23,7 +22,7 @@ pub struct MediumEditor {
|
|||
done: gtk::Image,
|
||||
name_entry: gtk::Entry,
|
||||
publish_switch: gtk::Switch,
|
||||
track_set_list: List,
|
||||
track_set_list: Rc<List>,
|
||||
track_sets: RefCell<Vec<TrackSetData>>,
|
||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||
}
|
||||
|
|
@ -45,8 +44,8 @@ impl MediumEditor {
|
|||
get_widget!(builder, gtk::Button, add_button);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
|
||||
let list = List::new("No recordings added.");
|
||||
frame.add(&list.widget);
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
backend,
|
||||
|
|
@ -106,25 +105,24 @@ impl MediumEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
this.track_set_list.set_make_widget(clone!(@strong this => move |index| {
|
||||
this.track_set_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let track_set = &this.track_sets.borrow()[index];
|
||||
|
||||
let title = track_set.recording.work.get_title();
|
||||
let subtitle = track_set.recording.get_performers();
|
||||
|
||||
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
|
||||
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
|
||||
let edit_button = gtk::Button::new();
|
||||
edit_button.set_relief(gtk::ReliefStyle::None);
|
||||
edit_button.set_has_frame(false);
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
edit_button.add(&edit_image);
|
||||
edit_button.set_child(Some(&edit_image));
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&title));
|
||||
row.set_subtitle(Some(&subtitle));
|
||||
row.add(&edit_button);
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
row.show_all();
|
||||
|
||||
edit_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
|
||||
|
|
@ -154,7 +152,7 @@ impl MediumEditor {
|
|||
|
||||
/// Save the medium and possibly upload it to the server.
|
||||
async fn save(self: Rc<Self>) -> Result<()> {
|
||||
let name = self.name_entry.get_text().to_string();
|
||||
let name = self.name_entry.get_text().unwrap().to_string();
|
||||
|
||||
// Create a new directory in the music library path for the imported medium.
|
||||
|
||||
|
|
@ -200,7 +198,7 @@ impl MediumEditor {
|
|||
|
||||
let medium = Medium {
|
||||
id: generate_id(),
|
||||
name: self.name_entry.get_text().to_string(),
|
||||
name: self.name_entry.get_text().unwrap().to_string(),
|
||||
discid: Some(self.source.discid.clone()),
|
||||
tracks: track_sets,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use super::medium_editor::MediumEditor;
|
|||
use super::disc_source::DiscSource;
|
||||
use crate::backend::Backend;
|
||||
use crate::widgets::{Navigator, NavigatorScreen};
|
||||
use anyhow::Result;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use crate::database::Recording;
|
||||
use crate::widgets::{Navigator, NavigatorScreen};
|
||||
use crate::widgets::new_list::List;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
|
|
@ -32,7 +31,7 @@ impl TrackEditor {
|
|||
parts_list.set_selection_mode(gtk::SelectionMode::None);
|
||||
parts_list.set_vexpand(false);
|
||||
parts_list.show();
|
||||
parts_frame.add(&parts_list);
|
||||
parts_frame.set_child(Some(&parts_list));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
|
|
@ -81,9 +80,8 @@ impl TrackEditor {
|
|||
row.add_prefix(&check);
|
||||
row.set_activatable_widget(Some(&check));
|
||||
row.set_title(Some(&part.title));
|
||||
row.show_all();
|
||||
|
||||
parts_list.add(&row);
|
||||
parts_list.append(&row);
|
||||
}
|
||||
|
||||
this
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ impl TrackSelector {
|
|||
track_list.set_selection_mode(gtk::SelectionMode::None);
|
||||
track_list.set_vexpand(false);
|
||||
track_list.show();
|
||||
tracks_frame.add(&track_list);
|
||||
tracks_frame.set_child(Some(&track_list));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
source,
|
||||
|
|
@ -90,10 +90,10 @@ impl TrackSelector {
|
|||
let row = libhandy::ActionRow::new();
|
||||
row.add_prefix(&check);
|
||||
row.set_activatable_widget(Some(&check));
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&title));
|
||||
row.show_all();
|
||||
|
||||
track_list.add(&row);
|
||||
track_list.append(&row);
|
||||
}
|
||||
|
||||
this
|
||||
|
|
|
|||
|
|
@ -2,17 +2,15 @@ use super::disc_source::DiscSource;
|
|||
use super::track_editor::TrackEditor;
|
||||
use super::track_selector::TrackSelector;
|
||||
use crate::backend::Backend;
|
||||
use crate::database::{Recording, Track, TrackSet};
|
||||
use crate::database::Recording;
|
||||
use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector};
|
||||
use crate::widgets::{Navigator, NavigatorScreen};
|
||||
use crate::widgets::new_list::List;
|
||||
use crate::widgets::{List, Navigator, NavigatorScreen};
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashSet;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A track set before being imported.
|
||||
|
|
@ -39,7 +37,7 @@ pub struct TrackSetEditor {
|
|||
widget: gtk::Box,
|
||||
save_button: gtk::Button,
|
||||
recording_row: libhandy::ActionRow,
|
||||
track_list: List,
|
||||
track_list: Rc<List>,
|
||||
recording: RefCell<Option<Recording>>,
|
||||
tracks: RefCell<Vec<TrackData>>,
|
||||
done_cb: RefCell<Option<Box<dyn Fn(TrackSetData)>>>,
|
||||
|
|
@ -61,8 +59,8 @@ impl TrackSetEditor {
|
|||
get_widget!(builder, gtk::Button, edit_tracks_button);
|
||||
get_widget!(builder, gtk::Frame, tracks_frame);
|
||||
|
||||
let track_list = List::new(&gettext!("No tracks added"));
|
||||
tracks_frame.add(&track_list.widget);
|
||||
let track_list = List::new();
|
||||
tracks_frame.set_child(Some(&track_list.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
backend,
|
||||
|
|
@ -159,7 +157,7 @@ impl TrackSetEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
this.track_list.set_make_widget(clone!(@strong this => move |index| {
|
||||
this.track_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let track = &this.tracks.borrow()[index];
|
||||
|
||||
let mut title_parts = Vec::<String>::new();
|
||||
|
|
@ -179,19 +177,18 @@ impl TrackSetEditor {
|
|||
let number = this.source.tracks[track.track_source].number;
|
||||
let subtitle = format!("Track {}", number);
|
||||
|
||||
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
|
||||
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
|
||||
let edit_button = gtk::Button::new();
|
||||
edit_button.set_relief(gtk::ReliefStyle::None);
|
||||
edit_button.set_has_frame(false);
|
||||
edit_button.set_valign(gtk::Align::Center);
|
||||
edit_button.add(&edit_image);
|
||||
edit_button.set_child(Some(&edit_image));
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&title));
|
||||
row.set_subtitle(Some(&subtitle));
|
||||
row.add(&edit_button);
|
||||
row.add_suffix(&edit_button);
|
||||
row.set_activatable_widget(Some(&edit_button));
|
||||
row.show_all();
|
||||
|
||||
edit_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let recording = this.recording.borrow().clone();
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@ sources = files(
|
|||
'editors/work.rs',
|
||||
'editors/work_part.rs',
|
||||
'editors/work_section.rs',
|
||||
'import/disc_source.rs',
|
||||
'import/medium_editor.rs',
|
||||
'import/mod.rs',
|
||||
'import/source_selector.rs',
|
||||
'import/track_editor.rs',
|
||||
'import/track_selector.rs',
|
||||
'import/track_set_editor.rs',
|
||||
'screens/ensemble_screen.rs',
|
||||
'screens/mod.rs',
|
||||
'screens/person_screen.rs',
|
||||
|
|
@ -78,13 +85,13 @@ sources = files(
|
|||
'selectors/recording.rs',
|
||||
'selectors/selector.rs',
|
||||
'selectors/work.rs',
|
||||
'widgets/indexed_list_model.rs',
|
||||
'widgets/list.rs',
|
||||
'widgets/mod.rs',
|
||||
'widgets/navigator.rs',
|
||||
'widgets/navigator_window.rs',
|
||||
'widgets/player_bar.rs',
|
||||
'widgets/poe_list.rs',
|
||||
'widgets/selector_row.rs',
|
||||
'config.rs',
|
||||
'config.rs.in',
|
||||
'main.rs',
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ use crate::backend::*;
|
|||
use crate::database::*;
|
||||
use crate::editors::EnsembleEditor;
|
||||
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
|
||||
use gettextrs::gettext;
|
||||
use gio::prelude::*;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::HeaderBarExt;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -16,8 +15,10 @@ pub struct EnsembleScreen {
|
|||
backend: Rc<Backend>,
|
||||
ensemble: Ensemble,
|
||||
widget: gtk::Box,
|
||||
search_entry: gtk::SearchEntry,
|
||||
stack: gtk::Stack,
|
||||
recording_list: Rc<List<Recording>>,
|
||||
recording_list: Rc<List>,
|
||||
recordings: RefCell<Vec<Recording>>,
|
||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||
}
|
||||
|
||||
|
|
@ -26,13 +27,13 @@ impl EnsembleScreen {
|
|||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, libhandy::HeaderBar, header);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::Frame, recording_frame);
|
||||
|
||||
header.set_title(Some(&ensemble.name));
|
||||
title_label.set_label(&ensemble.name);
|
||||
|
||||
let edit_action = gio::SimpleAction::new("edit", None);
|
||||
let delete_action = gio::SimpleAction::new("delete", None);
|
||||
|
|
@ -43,75 +44,66 @@ impl EnsembleScreen {
|
|||
|
||||
widget.insert_action_group("widget", Some(&actions));
|
||||
|
||||
let recording_list = List::new(&gettext("No recordings found."));
|
||||
let recording_list = List::new();
|
||||
recording_frame.set_child(Some(&recording_list.widget));
|
||||
|
||||
recording_list.set_make_widget(|recording: &Recording| {
|
||||
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
||||
|
||||
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
work_label.set_halign(gtk::Align::Start);
|
||||
|
||||
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);
|
||||
|
||||
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: &Recording| {
|
||||
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)
|
||||
}),
|
||||
);
|
||||
|
||||
recording_frame.add(&recording_list.widget.clone());
|
||||
|
||||
let result = Rc::new(Self {
|
||||
let this = Rc::new(Self {
|
||||
backend,
|
||||
ensemble,
|
||||
widget,
|
||||
search_entry,
|
||||
stack,
|
||||
recording_list,
|
||||
recordings: RefCell::new(Vec::new()),
|
||||
navigator: RefCell::new(None),
|
||||
});
|
||||
|
||||
search_entry.connect_search_changed(clone!(@strong result => move |_| {
|
||||
result.recording_list.invalidate_filter();
|
||||
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
|
||||
this.recording_list.invalidate_filter();
|
||||
}));
|
||||
|
||||
back_button.connect_clicked(clone!(@strong result => move |_| {
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.pop();
|
||||
}
|
||||
}));
|
||||
|
||||
result
|
||||
.recording_list
|
||||
.set_selected(clone!(@strong result => move |recording| {
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.push(RecordingScreen::new(result.backend.clone(), recording.clone()));
|
||||
navigator.push(RecordingScreen::new(this.backend.clone(), recording.clone()));
|
||||
}
|
||||
}));
|
||||
|
||||
edit_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
let editor = EnsembleEditor::new(result.backend.clone(), Some(result.ensemble.clone()));
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.recording_list.set_filter_cb(clone!(@strong this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
let text = recording.work.get_title() + &recording.get_performers();
|
||||
search.is_empty() || text.to_lowercase().contains(&search)
|
||||
}));
|
||||
|
||||
edit_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let editor = EnsembleEditor::new(this.backend.clone(), Some(this.ensemble.clone()));
|
||||
let window = NavigatorWindow::new(editor);
|
||||
window.show();
|
||||
}));
|
||||
|
||||
delete_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
delete_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
clone.backend.db().delete_ensemble(&clone.ensemble.id).await.unwrap();
|
||||
clone.backend.library_changed();
|
||||
|
|
@ -119,7 +111,7 @@ impl EnsembleScreen {
|
|||
}));
|
||||
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
let recordings = clone
|
||||
.backend
|
||||
|
|
@ -131,12 +123,14 @@ impl EnsembleScreen {
|
|||
if recordings.is_empty() {
|
||||
clone.stack.set_visible_child_name("nothing");
|
||||
} else {
|
||||
clone.recording_list.show_items(recordings);
|
||||
let length = recordings.len();
|
||||
clone.recordings.replace(recordings);
|
||||
clone.recording_list.update(length);
|
||||
clone.stack.set_visible_child_name("content");
|
||||
}
|
||||
});
|
||||
|
||||
result
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ use crate::backend::*;
|
|||
use crate::database::*;
|
||||
use crate::editors::PersonEditor;
|
||||
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
|
||||
use gettextrs::gettext;
|
||||
use gio::prelude::*;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::HeaderBarExt;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -17,8 +16,13 @@ pub struct PersonScreen {
|
|||
person: Person,
|
||||
widget: gtk::Box,
|
||||
stack: gtk::Stack,
|
||||
work_list: Rc<List<Work>>,
|
||||
recording_list: Rc<List<Recording>>,
|
||||
search_entry: gtk::SearchEntry,
|
||||
work_box: gtk::Box,
|
||||
work_list: Rc<List>,
|
||||
recording_box: gtk::Box,
|
||||
recording_list: Rc<List>,
|
||||
works: RefCell<Vec<Work>>,
|
||||
recordings: RefCell<Vec<Recording>>,
|
||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +31,7 @@ impl PersonScreen {
|
|||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, libhandy::HeaderBar, header);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
|
|
@ -36,7 +40,7 @@ impl PersonScreen {
|
|||
get_widget!(builder, gtk::Box, recording_box);
|
||||
get_widget!(builder, gtk::Frame, recording_frame);
|
||||
|
||||
header.set_title(Some(&person.name_fl()));
|
||||
title_label.set_label(&person.name_fl());
|
||||
|
||||
let edit_action = gio::SimpleAction::new("edit", None);
|
||||
let delete_action = gio::SimpleAction::new("delete", None);
|
||||
|
|
@ -47,107 +51,98 @@ impl PersonScreen {
|
|||
|
||||
widget.insert_action_group("widget", Some(&actions));
|
||||
|
||||
let work_list = List::new(&gettext("No works found."));
|
||||
let work_list = List::new();
|
||||
let recording_list = List::new();
|
||||
work_frame.set_child(Some(&work_list.widget));
|
||||
recording_frame.set_child(Some(&recording_list.widget));
|
||||
|
||||
work_list.set_make_widget(|work: &Work| {
|
||||
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: &Work| {
|
||||
let search = search_entry.get_text().to_string().to_lowercase();
|
||||
let title = work.title.to_lowercase();
|
||||
search.is_empty() || title.contains(&search)
|
||||
}));
|
||||
|
||||
let recording_list = List::new(&gettext("No recordings found."));
|
||||
|
||||
recording_list.set_make_widget(|recording: &Recording| {
|
||||
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
||||
|
||||
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
work_label.set_halign(gtk::Align::Start);
|
||||
|
||||
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);
|
||||
|
||||
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: &Recording| {
|
||||
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)
|
||||
}),
|
||||
);
|
||||
|
||||
work_frame.add(&work_list.widget);
|
||||
recording_frame.add(&recording_list.widget);
|
||||
|
||||
let result = Rc::new(Self {
|
||||
let this = Rc::new(Self {
|
||||
backend,
|
||||
person,
|
||||
widget,
|
||||
stack,
|
||||
search_entry,
|
||||
work_box,
|
||||
work_list,
|
||||
recording_box,
|
||||
recording_list,
|
||||
works: RefCell::new(Vec::new()),
|
||||
recordings: RefCell::new(Vec::new()),
|
||||
navigator: RefCell::new(None),
|
||||
});
|
||||
|
||||
search_entry.connect_search_changed(clone!(@strong result => move |_| {
|
||||
result.work_list.invalidate_filter();
|
||||
result.recording_list.invalidate_filter();
|
||||
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
|
||||
this.work_list.invalidate_filter();
|
||||
this.recording_list.invalidate_filter();
|
||||
}));
|
||||
|
||||
back_button.connect_clicked(clone!(@strong result => move |_| {
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.clone().pop();
|
||||
}
|
||||
}));
|
||||
|
||||
result
|
||||
.work_list
|
||||
.set_selected(clone!(@strong result => move |work| {
|
||||
result.recording_list.clear_selection();
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
this.work_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let work = &this.works.borrow()[index];
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&work.title));
|
||||
|
||||
let work = work.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.push(WorkScreen::new(result.backend.clone(), work.clone()));
|
||||
navigator.push(WorkScreen::new(this.backend.clone(), work.clone()));
|
||||
}
|
||||
}));
|
||||
|
||||
result
|
||||
.recording_list
|
||||
.set_selected(clone!(@strong result => move |recording| {
|
||||
result.work_list.clear_selection();
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.work_list.set_filter_cb(clone!(@strong this => move |index| {
|
||||
let work = &this.works.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
let title = work.title.to_lowercase();
|
||||
search.is_empty() || title.contains(&search)
|
||||
}));
|
||||
|
||||
this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.push(RecordingScreen::new(result.backend.clone(), recording.clone()));
|
||||
navigator.push(RecordingScreen::new(this.backend.clone(), recording.clone()));
|
||||
}
|
||||
}));
|
||||
|
||||
edit_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
let editor = PersonEditor::new(result.backend.clone(), Some(result.person.clone()));
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.recording_list.set_filter_cb(clone!(@strong this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
let text = recording.work.get_title() + &recording.get_performers();
|
||||
search.is_empty() || text.contains(&search)
|
||||
}));
|
||||
|
||||
edit_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let editor = PersonEditor::new(this.backend.clone(), Some(this.person.clone()));
|
||||
let window = NavigatorWindow::new(editor);
|
||||
window.show();
|
||||
}));
|
||||
|
||||
delete_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
delete_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
clone.backend.db().delete_person(&clone.person.id).await.unwrap();
|
||||
clone.backend.library_changed();
|
||||
|
|
@ -155,7 +150,7 @@ impl PersonScreen {
|
|||
}));
|
||||
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
let works = clone
|
||||
.backend
|
||||
|
|
@ -163,6 +158,7 @@ impl PersonScreen {
|
|||
.get_works(&clone.person.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let recordings = clone
|
||||
.backend
|
||||
.db()
|
||||
|
|
@ -174,22 +170,26 @@ impl PersonScreen {
|
|||
clone.stack.set_visible_child_name("nothing");
|
||||
} else {
|
||||
if works.is_empty() {
|
||||
work_box.hide();
|
||||
clone.work_box.hide();
|
||||
} else {
|
||||
clone.work_list.show_items(works);
|
||||
let length = works.len();
|
||||
clone.works.replace(works);
|
||||
clone.work_list.update(length);
|
||||
}
|
||||
|
||||
if recordings.is_empty() {
|
||||
recording_box.hide();
|
||||
clone.recording_box.hide();
|
||||
} else {
|
||||
clone.recording_list.show_items(recordings);
|
||||
let length = recordings.len();
|
||||
clone.recordings.replace(recordings);
|
||||
clone.recording_list.update(length);
|
||||
}
|
||||
|
||||
clone.stack.set_visible_child_name("content");
|
||||
}
|
||||
});
|
||||
|
||||
result
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,22 @@ use gettextrs::gettext;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
struct PlaylistElement {
|
||||
pub item: usize,
|
||||
pub track: usize,
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub playable: bool,
|
||||
/// Elements for visually representing the playlist.
|
||||
enum ListItem {
|
||||
/// A header shown on top of a track set. This contains an index
|
||||
/// referencing the playlist item containing this track set.
|
||||
Header(usize),
|
||||
|
||||
/// A playable track. This contains an index to the playlist item, an
|
||||
/// index to the track and whether it is the currently played one.
|
||||
Track(usize, usize, bool),
|
||||
|
||||
/// A separator shown between track sets.
|
||||
Separator,
|
||||
}
|
||||
|
||||
pub struct PlayerScreen {
|
||||
|
|
@ -27,16 +34,18 @@ pub struct PlayerScreen {
|
|||
duration_label: gtk::Label,
|
||||
play_image: gtk::Image,
|
||||
pause_image: gtk::Image,
|
||||
list: Rc<List<PlaylistElement>>,
|
||||
player: Rc<RefCell<Option<Rc<Player>>>>,
|
||||
seeking: Rc<Cell<bool>>,
|
||||
current_item: Rc<Cell<usize>>,
|
||||
current_track: Rc<Cell<usize>>,
|
||||
back_cb: Rc<RefCell<Option<Box<dyn Fn() -> ()>>>>,
|
||||
list: Rc<List>,
|
||||
playlist: RefCell<Vec<PlaylistItem>>,
|
||||
items: RefCell<Vec<ListItem>>,
|
||||
player: RefCell<Option<Rc<Player>>>,
|
||||
seeking: Cell<bool>,
|
||||
current_item: Cell<usize>,
|
||||
current_track: Cell<usize>,
|
||||
back_cb: RefCell<Option<Box<dyn Fn()>>>,
|
||||
}
|
||||
|
||||
impl PlayerScreen {
|
||||
pub fn new() -> Self {
|
||||
pub fn new() -> Rc<Self> {
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
|
|
@ -55,124 +64,10 @@ impl PlayerScreen {
|
|||
get_widget!(builder, gtk::Image, pause_image);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
|
||||
let back_cb = Rc::new(RefCell::new(None::<Box<dyn Fn() -> ()>>));
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
back_button.connect_clicked(clone!(@strong back_cb => move |_| {
|
||||
if let Some(cb) = &*back_cb.borrow() {
|
||||
cb();
|
||||
}
|
||||
}));
|
||||
|
||||
let player = Rc::new(RefCell::new(None::<Rc<Player>>));
|
||||
let seeking = Rc::new(Cell::new(false));
|
||||
|
||||
previous_button.connect_clicked(clone!(@strong player => move |_| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
player.previous().unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
play_button.connect_clicked(clone!(@strong player => move |_| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
player.play_pause();
|
||||
}
|
||||
}));
|
||||
|
||||
next_button.connect_clicked(clone!(@strong player => move |_| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
player.next().unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
stop_button.connect_clicked(clone!(@strong player, @strong back_cb => move |_| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
if let Some(cb) = &*back_cb.borrow() {
|
||||
cb();
|
||||
}
|
||||
|
||||
player.clear();
|
||||
}
|
||||
}));
|
||||
|
||||
position_scale.connect_button_press_event(clone!(@strong seeking => move |_, _| {
|
||||
seeking.replace(true);
|
||||
Inhibit(false)
|
||||
}));
|
||||
|
||||
position_scale.connect_button_release_event(
|
||||
clone!(@strong seeking, @strong position, @strong player => move |_, _| {
|
||||
if let Some(player) = &*player.borrow() {
|
||||
player.seek(position.get_value() as u64);
|
||||
}
|
||||
|
||||
seeking.replace(false);
|
||||
Inhibit(false)
|
||||
}),
|
||||
);
|
||||
|
||||
position_scale.connect_value_changed(
|
||||
clone!(@strong seeking, @strong position, @strong position_label => move |_| {
|
||||
if seeking.get() {
|
||||
let ms = position.get_value() as u64;
|
||||
let min = ms / 60000;
|
||||
let sec = (ms % 60000) / 1000;
|
||||
position_label.set_text(&format!("{}:{:02}", min, sec));
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let current_item = Rc::new(Cell::<usize>::new(0));
|
||||
let current_track = Rc::new(Cell::<usize>::new(0));
|
||||
let list = List::new("");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
player.set_track(element.item, element.track).unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
frame.add(&list.widget);
|
||||
|
||||
Self {
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
title_label,
|
||||
subtitle_label,
|
||||
|
|
@ -185,106 +80,222 @@ impl PlayerScreen {
|
|||
play_image,
|
||||
pause_image,
|
||||
list,
|
||||
player,
|
||||
seeking,
|
||||
current_item,
|
||||
current_track,
|
||||
back_cb,
|
||||
}
|
||||
}
|
||||
items: RefCell::new(Vec::new()),
|
||||
playlist: RefCell::new(Vec::new()),
|
||||
player: RefCell::new(None),
|
||||
seeking: Cell::new(false),
|
||||
current_item: Cell::new(0),
|
||||
current_track: Cell::new(0),
|
||||
back_cb: RefCell::new(None),
|
||||
});
|
||||
|
||||
pub fn set_player(&self, player: Option<Rc<Player>>) {
|
||||
self.player.replace(player.clone());
|
||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(cb) = &*this.back_cb.borrow() {
|
||||
cb();
|
||||
}
|
||||
}));
|
||||
|
||||
if let Some(player) = player {
|
||||
let playlist = Rc::new(RefCell::new(Vec::<PlaylistItem>::new()));
|
||||
this.previous_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(player) = &*this.player.borrow() {
|
||||
player.previous().unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
player.add_playlist_cb(clone!(
|
||||
@strong player,
|
||||
@strong self.previous_button as previous_button,
|
||||
@strong self.next_button as next_button,
|
||||
@strong self.list as list,
|
||||
@strong playlist
|
||||
=> move |new_playlist| {
|
||||
playlist.replace(new_playlist);
|
||||
previous_button.set_sensitive(player.has_previous());
|
||||
next_button.set_sensitive(player.has_next());
|
||||
this.play_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(player) = &*this.player.borrow() {
|
||||
player.play_pause();
|
||||
}
|
||||
}));
|
||||
|
||||
let mut elements = Vec::new();
|
||||
for (item_index, item) in playlist.borrow().iter().enumerate() {
|
||||
elements.push(PlaylistElement {
|
||||
item: item_index,
|
||||
track: 0,
|
||||
title: item.track_set.recording.work.get_title(),
|
||||
subtitle: Some(item.track_set.recording.get_performers()),
|
||||
playable: false,
|
||||
});
|
||||
this.next_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(player) = &*this.player.borrow() {
|
||||
player.next().unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
for track_index in &item.indices {
|
||||
let track = &item.track_set.tracks[*track_index];
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let title = if parts.is_empty() {
|
||||
gettext("Unknown")
|
||||
} else {
|
||||
parts.join(", ")
|
||||
};
|
||||
|
||||
elements.push(PlaylistElement {
|
||||
item: item_index,
|
||||
track: *track_index,
|
||||
title: title,
|
||||
subtitle: None,
|
||||
playable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
list.show_items(elements);
|
||||
stop_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(player) = &*this.player.borrow() {
|
||||
if let Some(cb) = &*this.back_cb.borrow() {
|
||||
cb();
|
||||
}
|
||||
));
|
||||
|
||||
player.add_track_cb(clone!(
|
||||
@strong player,
|
||||
@strong playlist,
|
||||
@strong self.previous_button as previous_button,
|
||||
@strong self.next_button as next_button,
|
||||
@strong self.title_label as title_label,
|
||||
@strong self.subtitle_label as subtitle_label,
|
||||
@strong self.position_label as position_label,
|
||||
@strong self.current_item as self_item,
|
||||
@strong self.current_track as self_track,
|
||||
@strong self.list as list
|
||||
=> move |current_item, current_track| {
|
||||
previous_button.set_sensitive(player.has_previous());
|
||||
next_button.set_sensitive(player.has_next());
|
||||
player.clear();
|
||||
}
|
||||
}));
|
||||
|
||||
let item = &playlist.borrow()[current_item];
|
||||
let track = &item.track_set.tracks[current_track];
|
||||
// position_scale.connect_button_press_event(clone!(@strong seeking => move |_, _| {
|
||||
// seeking.replace(true);
|
||||
// Inhibit(false)
|
||||
// }));
|
||||
|
||||
// position_scale.connect_button_release_event(
|
||||
// clone!(@strong seeking, @strong position, @strong player => move |_, _| {
|
||||
// if let Some(player) = &*player.borrow() {
|
||||
// player.seek(position.get_value() as u64);
|
||||
// }
|
||||
|
||||
// seeking.replace(false);
|
||||
// Inhibit(false)
|
||||
// }),
|
||||
// );
|
||||
|
||||
position_scale.connect_value_changed(clone!(@strong this => move |_| {
|
||||
if this.seeking.get() {
|
||||
let ms = this.position.get_value() as u64;
|
||||
let min = ms / 60000;
|
||||
let sec = (ms % 60000) / 1000;
|
||||
|
||||
this.position_label.set_text(&format!("{}:{:02}", min, sec));
|
||||
}
|
||||
}));
|
||||
|
||||
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
match this.items.borrow()[index] {
|
||||
ListItem::Header(item_index) => {
|
||||
let playlist_item = &this.playlist.borrow()[item_index];
|
||||
let recording = &playlist_item.track_set.recording;
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(false);
|
||||
row.set_selectable(false);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
ListItem::Track(item_index, track_index, playing) => {
|
||||
let playlist_item = &this.playlist.borrow()[item_index];
|
||||
let index = playlist_item.indices[track_index];
|
||||
let track = &playlist_item.track_set.tracks[index];
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||
parts.push(playlist_item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let mut title = item.track_set.recording.work.get_title();
|
||||
if !parts.is_empty() {
|
||||
title = format!("{}: {}", title, parts.join(", "));
|
||||
}
|
||||
let title = if parts.is_empty() {
|
||||
gettext("Unknown")
|
||||
} else {
|
||||
parts.join(", ")
|
||||
};
|
||||
|
||||
title_label.set_text(&title);
|
||||
subtitle_label.set_text(&item.track_set.recording.get_performers());
|
||||
position_label.set_text("0:00");
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_selectable(false);
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&title));
|
||||
|
||||
self_item.replace(current_item);
|
||||
self_track.replace(current_track);
|
||||
list.update();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
if let Some(player) = &*this.player.borrow() {
|
||||
player.set_track(item_index, track_index).unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
let icon = if playing {
|
||||
Some("media-playback-start-symbolic")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let image = gtk::Image::from_icon_name(icon);
|
||||
row.add_prefix(&image);
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
));
|
||||
ListItem::Separator => {
|
||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
||||
separator.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.append(&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.append(&subtitle_label);
|
||||
// }
|
||||
|
||||
// let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
// hbox.set_margin_top(6);
|
||||
// hbox.set_margin_bottom(6);
|
||||
// hbox.set_margin_start(6);
|
||||
// hbox.set_margin_end(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.append(&image);
|
||||
// } else if element.item > 0 {
|
||||
// hbox.set_margin_top(18);
|
||||
// }
|
||||
// hbox.append(&vbox);
|
||||
// hbox.upcast()
|
||||
// }
|
||||
// ));
|
||||
|
||||
// list.set_selected(clone!(@strong player => move |element| {
|
||||
// if let Some(player) = &*player.borrow() {
|
||||
// player.set_track(element.item, element.track).unwrap();
|
||||
// }
|
||||
// }));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn set_player(self: Rc<Self>, player: Option<Rc<Player>>) {
|
||||
self.player.replace(player.clone());
|
||||
|
||||
if let Some(player) = player {
|
||||
player.add_playlist_cb(clone!(@strong self as this => move |playlist| {
|
||||
this.playlist.replace(playlist);
|
||||
this.show_playlist();
|
||||
}));
|
||||
|
||||
player.add_track_cb(clone!(@strong self as this, @strong player => move |current_item, current_track| {
|
||||
this.previous_button.set_sensitive(player.has_previous());
|
||||
this.next_button.set_sensitive(player.has_next());
|
||||
|
||||
let item = &this.playlist.borrow()[current_item];
|
||||
let track = &item.track_set.tracks[current_track];
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let mut title = item.track_set.recording.work.get_title();
|
||||
if !parts.is_empty() {
|
||||
title = format!("{}: {}", title, parts.join(", "));
|
||||
}
|
||||
|
||||
this.title_label.set_text(&title);
|
||||
this.subtitle_label.set_text(&item.track_set.recording.get_performers());
|
||||
this.position_label.set_text("0:00");
|
||||
|
||||
this.current_item.set(current_item);
|
||||
this.current_track.set(current_track);
|
||||
|
||||
this.show_playlist();
|
||||
}));
|
||||
|
||||
player.add_duration_cb(clone!(
|
||||
@strong self.duration_label as duration_label,
|
||||
|
|
@ -302,15 +313,11 @@ impl PlayerScreen {
|
|||
@strong self.play_image as play_image,
|
||||
@strong self.pause_image as pause_image
|
||||
=> move |playing| {
|
||||
if let Some(child) = play_button.get_child() {
|
||||
play_button.remove( &child);
|
||||
}
|
||||
|
||||
play_button.add(if playing {
|
||||
play_button.set_child(Some(if playing {
|
||||
&pause_image
|
||||
} else {
|
||||
&play_image
|
||||
});
|
||||
}));
|
||||
}
|
||||
));
|
||||
|
||||
|
|
@ -333,4 +340,33 @@ impl PlayerScreen {
|
|||
pub fn set_back_cb<F: Fn() -> () + 'static>(&self, cb: F) {
|
||||
self.back_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Update the user interface according to the playlist.
|
||||
fn show_playlist(&self) {
|
||||
let playlist = self.playlist.borrow();
|
||||
let current_item = self.current_item.get();
|
||||
let current_track = self.current_track.get();
|
||||
|
||||
let mut first = true;
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (item_index, playlist_item) in playlist.iter().enumerate() {
|
||||
if !first {
|
||||
items.push(ListItem::Separator);
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
items.push(ListItem::Header(item_index));
|
||||
|
||||
for (index, _) in playlist_item.indices.iter().enumerate() {
|
||||
let playing = current_item == item_index && current_track == index;
|
||||
items.push(ListItem::Track(item_index, index, playing));
|
||||
}
|
||||
}
|
||||
|
||||
let length = items.len();
|
||||
self.items.replace(items);
|
||||
self.list.update(length);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,28 @@ use gio::prelude::*;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::HeaderBarExt;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Representation of one entry within the track list.
|
||||
enum ListItem {
|
||||
/// A track row. This hold an index to the track set and an index to the
|
||||
/// track within the track set.
|
||||
Track(usize, usize),
|
||||
|
||||
/// A separator intended for use between track sets.
|
||||
Separator,
|
||||
}
|
||||
|
||||
pub struct RecordingScreen {
|
||||
backend: Rc<Backend>,
|
||||
recording: Recording,
|
||||
widget: gtk::Box,
|
||||
stack: gtk::Stack,
|
||||
list: Rc<List>,
|
||||
track_sets: RefCell<Vec<TrackSet>>,
|
||||
items: RefCell<Vec<ListItem>>,
|
||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||
}
|
||||
|
||||
|
|
@ -26,14 +38,15 @@ impl RecordingScreen {
|
|||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, libhandy::HeaderBar, header);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Label, subtitle_label);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
get_widget!(builder, gtk::Button, add_to_playlist_button);
|
||||
|
||||
header.set_title(Some(&recording.work.get_title()));
|
||||
header.set_subtitle(Some(&recording.get_performers()));
|
||||
title_label.set_label(&recording.work.get_title());
|
||||
subtitle_label.set_label(&recording.get_performers());
|
||||
|
||||
let edit_action = gio::SimpleAction::new("edit", None);
|
||||
let delete_action = gio::SimpleAction::new("delete", None);
|
||||
|
|
@ -48,95 +61,96 @@ impl RecordingScreen {
|
|||
|
||||
widget.insert_action_group("widget", Some(&actions));
|
||||
|
||||
let list = List::new(&gettext("No tracks found."));
|
||||
frame.add(&list.widget);
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
let result = Rc::new(Self {
|
||||
let this = Rc::new(Self {
|
||||
backend,
|
||||
recording,
|
||||
widget,
|
||||
stack,
|
||||
list,
|
||||
track_sets: RefCell::new(Vec::new()),
|
||||
items: RefCell::new(Vec::new()),
|
||||
navigator: RefCell::new(None),
|
||||
});
|
||||
|
||||
list.set_make_widget(clone!(@strong result => move |track_set: &TrackSet| {
|
||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
vbox.set_border_width(6);
|
||||
vbox.set_spacing(6);
|
||||
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
match this.items.borrow()[index] {
|
||||
ListItem::Track(track_set_index, track_index) => {
|
||||
let track_set = &this.track_sets.borrow()[track_set_index];
|
||||
let track = &track_set.tracks[track_index];
|
||||
|
||||
for track in &track_set.tracks {
|
||||
let track_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
let mut title_parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
title_parts.push(this.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let mut title_parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
title_parts.push(result.recording.work.parts[*part].title.clone());
|
||||
let title = if title_parts.is_empty() {
|
||||
gettext("Unknown")
|
||||
} else {
|
||||
title_parts.join(", ")
|
||||
};
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_title(Some(&title));
|
||||
|
||||
row.upcast()
|
||||
}
|
||||
ListItem::Separator => {
|
||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
||||
separator.upcast()
|
||||
}
|
||||
|
||||
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.path));
|
||||
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
file_name_label.set_opacity(0.5);
|
||||
file_name_label.set_halign(gtk::Align::Start);
|
||||
|
||||
track_box.add(&title_label);
|
||||
track_box.add(&file_name_label);
|
||||
|
||||
vbox.add(&track_box);
|
||||
}
|
||||
|
||||
vbox.upcast()
|
||||
}));
|
||||
|
||||
back_button.connect_clicked(clone!(@strong result => move |_| {
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.clone().pop();
|
||||
}
|
||||
}));
|
||||
|
||||
add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| {
|
||||
// if let Some(player) = result.backend.get_player() {
|
||||
// player.add_item(PlaylistItem {
|
||||
// track_set: result.track_sets.get(0).unwrap().clone(),
|
||||
// indices: result.tracks.borrow().clone(),
|
||||
// }).unwrap();
|
||||
// }
|
||||
// TODO: Decide whether to handle multiple track sets.
|
||||
add_to_playlist_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
if let Some(player) = this.backend.get_player() {
|
||||
if let Some(track_set) = this.track_sets.borrow().get(0).cloned() {
|
||||
let indices = (0..track_set.tracks.len()).collect();
|
||||
|
||||
let playlist_item = PlaylistItem {
|
||||
track_set,
|
||||
indices,
|
||||
};
|
||||
|
||||
player.add_item(playlist_item).unwrap();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
edit_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
let editor = RecordingEditor::new(result.backend.clone(), Some(result.recording.clone()));
|
||||
edit_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let editor = RecordingEditor::new(this.backend.clone(), Some(this.recording.clone()));
|
||||
let window = NavigatorWindow::new(editor);
|
||||
window.show();
|
||||
}));
|
||||
|
||||
delete_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
delete_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
clone.backend.db().delete_recording(&clone.recording.id).await.unwrap();
|
||||
clone.backend.library_changed();
|
||||
});
|
||||
}));
|
||||
|
||||
edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
// let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone());
|
||||
edit_tracks_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
// let editor = TracksEditor::new(this.backend.clone(), Some(this.recording.clone()), this.tracks.borrow().clone());
|
||||
// let window = NavigatorWindow::new(editor);
|
||||
// window.show();
|
||||
}));
|
||||
|
||||
delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
delete_tracks_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
// clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
|
||||
// clone.backend.library_changed();
|
||||
|
|
@ -144,7 +158,7 @@ impl RecordingScreen {
|
|||
}));
|
||||
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
let track_sets = clone
|
||||
.backend
|
||||
|
|
@ -153,12 +167,34 @@ impl RecordingScreen {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
list.show_items(track_sets.clone());
|
||||
clone.show_track_sets(track_sets);
|
||||
clone.stack.set_visible_child_name("content");
|
||||
clone.track_sets.replace(track_sets);
|
||||
});
|
||||
|
||||
result
|
||||
this
|
||||
}
|
||||
|
||||
/// Update the track sets variable as well as the user interface.
|
||||
fn show_track_sets(&self, track_sets: Vec<TrackSet>) {
|
||||
let mut first = true;
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (track_set_index, track_set) in track_sets.iter().enumerate() {
|
||||
if !first {
|
||||
items.push(ListItem::Separator);
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
for (track_index, _) in track_set.tracks.iter().enumerate() {
|
||||
items.push(ListItem::Track(track_set_index, track_index));
|
||||
}
|
||||
}
|
||||
|
||||
let length = items.len();
|
||||
self.items.replace(items);
|
||||
self.track_sets.replace(track_sets);
|
||||
self.list.update(length);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ use crate::backend::*;
|
|||
use crate::database::*;
|
||||
use crate::editors::WorkEditor;
|
||||
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
|
||||
use gettextrs::gettext;
|
||||
use gio::prelude::*;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::HeaderBarExt;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -17,7 +16,9 @@ pub struct WorkScreen {
|
|||
work: Work,
|
||||
widget: gtk::Box,
|
||||
stack: gtk::Stack,
|
||||
recording_list: Rc<List<Recording>>,
|
||||
search_entry: gtk::SearchEntry,
|
||||
recording_list: Rc<List>,
|
||||
recordings: RefCell<Vec<Recording>>,
|
||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||
}
|
||||
|
||||
|
|
@ -26,14 +27,15 @@ impl WorkScreen {
|
|||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, libhandy::HeaderBar, header);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Label, subtitle_label);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::Frame, recording_frame);
|
||||
|
||||
header.set_title(Some(&work.title));
|
||||
header.set_subtitle(Some(&work.composer.name_fl()));
|
||||
title_label.set_label(&work.composer.name_fl());
|
||||
subtitle_label.set_label(&work.title);
|
||||
|
||||
let edit_action = gio::SimpleAction::new("edit", None);
|
||||
let delete_action = gio::SimpleAction::new("delete", None);
|
||||
|
|
@ -44,73 +46,66 @@ impl WorkScreen {
|
|||
|
||||
widget.insert_action_group("widget", Some(&actions));
|
||||
|
||||
let recording_list = List::new(&gettext("No recordings found."));
|
||||
let recording_list = List::new();
|
||||
recording_frame.set_child(Some(&recording_list.widget));
|
||||
|
||||
recording_list.set_make_widget(|recording: &Recording| {
|
||||
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
||||
|
||||
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
work_label.set_halign(gtk::Align::Start);
|
||||
|
||||
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);
|
||||
|
||||
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: &Recording| {
|
||||
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);
|
||||
|
||||
let result = Rc::new(Self {
|
||||
let this = Rc::new(Self {
|
||||
backend,
|
||||
work,
|
||||
widget,
|
||||
stack,
|
||||
search_entry,
|
||||
recording_list,
|
||||
recordings: RefCell::new(Vec::new()),
|
||||
navigator: RefCell::new(None),
|
||||
});
|
||||
|
||||
search_entry.connect_search_changed(clone!(@strong result => move |_| {
|
||||
result.recording_list.invalidate_filter();
|
||||
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
|
||||
this.recording_list.invalidate_filter();
|
||||
}));
|
||||
|
||||
back_button.connect_clicked(clone!(@strong result => move |_| {
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.clone().pop();
|
||||
}
|
||||
}));
|
||||
|
||||
result
|
||||
.recording_list
|
||||
.set_selected(clone!(@strong result => move |recording| {
|
||||
let navigator = result.navigator.borrow().clone();
|
||||
this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.work.get_title()));
|
||||
row.set_subtitle(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
let navigator = this.navigator.borrow().clone();
|
||||
if let Some(navigator) = navigator {
|
||||
navigator.push(RecordingScreen::new(result.backend.clone(), recording.clone()));
|
||||
navigator.push(RecordingScreen::new(this.backend.clone(), recording.clone()));
|
||||
}
|
||||
}));
|
||||
|
||||
edit_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
let editor = WorkEditor::new(result.backend.clone(), Some(result.work.clone()));
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.recording_list.set_filter_cb(clone!(@strong this => move |index| {
|
||||
let recording = &this.recordings.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
let text = recording.work.get_title() + &recording.get_performers();
|
||||
search.is_empty() || text.to_lowercase().contains(&search)
|
||||
}));
|
||||
|
||||
edit_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let editor = WorkEditor::new(this.backend.clone(), Some(this.work.clone()));
|
||||
let window = NavigatorWindow::new(editor);
|
||||
window.show();
|
||||
}));
|
||||
|
||||
delete_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||
delete_action.connect_activate(clone!(@strong this => move |_, _| {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
clone.backend.db().delete_work(&clone.work.id).await.unwrap();
|
||||
clone.backend.library_changed();
|
||||
|
|
@ -118,7 +113,7 @@ impl WorkScreen {
|
|||
}));
|
||||
|
||||
let context = glib::MainContext::default();
|
||||
let clone = result.clone();
|
||||
let clone = this.clone();
|
||||
context.spawn_local(async move {
|
||||
let recordings = clone
|
||||
.backend
|
||||
|
|
@ -130,12 +125,14 @@ impl WorkScreen {
|
|||
if recordings.is_empty() {
|
||||
clone.stack.set_visible_child_name("nothing");
|
||||
} else {
|
||||
clone.recording_list.show_items(recordings);
|
||||
let length = recordings.len();
|
||||
clone.recordings.replace(recordings);
|
||||
clone.recording_list.update(length);
|
||||
clone.stack.set_visible_child_name("content");
|
||||
}
|
||||
});
|
||||
|
||||
result
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::widgets::{Navigator, NavigatorScreen};
|
|||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -63,22 +64,22 @@ impl EnsembleSelector {
|
|||
async move { clone.backend.db().get_ensembles().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(|ensemble| {
|
||||
let label = gtk::Label::new(Some(&ensemble.name));
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_margin_start(6);
|
||||
label.set_margin_end(6);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
label.upcast()
|
||||
});
|
||||
this.selector.set_make_widget(clone!(@strong this => move |ensemble| {
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&ensemble.name));
|
||||
|
||||
let ensemble = ensemble.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
this.select(&ensemble);
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, ensemble| ensemble.name.to_lowercase().contains(search));
|
||||
|
||||
this.selector
|
||||
.set_selected_cb(clone!(@strong this => move |ensemble| this.select(ensemble)));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +88,7 @@ impl EnsembleSelector {
|
|||
self.selected_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Select a ensemble.
|
||||
/// Select an ensemble.
|
||||
fn select(&self, ensemble: &Ensemble) {
|
||||
if let Some(cb) = &*self.selected_cb.borrow() {
|
||||
cb(&ensemble);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::widgets::{Navigator, NavigatorScreen};
|
|||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -63,22 +64,22 @@ impl InstrumentSelector {
|
|||
async move { clone.backend.db().get_instruments().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(|instrument| {
|
||||
let label = gtk::Label::new(Some(&instrument.name));
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_margin_start(6);
|
||||
label.set_margin_end(6);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
label.upcast()
|
||||
});
|
||||
this.selector.set_make_widget(clone!(@strong this => move |instrument| {
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&instrument.name));
|
||||
|
||||
let instrument = instrument.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
this.select(&instrument);
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, instrument| instrument.name.to_lowercase().contains(search));
|
||||
|
||||
this.selector
|
||||
.set_selected_cb(clone!(@strong this => move |instrument| this.select(instrument)));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +88,7 @@ impl InstrumentSelector {
|
|||
self.selected_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Select a instrument.
|
||||
/// Select an instrument.
|
||||
fn select(&self, instrument: &Instrument) {
|
||||
if let Some(cb) = &*self.selected_cb.borrow() {
|
||||
cb(&instrument);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::widgets::{Navigator, NavigatorScreen};
|
|||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -63,22 +64,22 @@ impl PersonSelector {
|
|||
async move { clone.backend.db().get_persons().await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(|person| {
|
||||
let label = gtk::Label::new(Some(&person.name_lf()));
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_margin_start(6);
|
||||
label.set_margin_end(6);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
label.upcast()
|
||||
});
|
||||
this.selector.set_make_widget(clone!(@strong this => move |person| {
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&person.name_lf()));
|
||||
|
||||
let person = person.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
this.select(&person);
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
|
||||
|
||||
this.selector
|
||||
.set_selected_cb(clone!(@strong this => move |person| this.select(person)));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::widgets::{Navigator, NavigatorScreen};
|
|||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -73,23 +74,23 @@ impl RecordingSelector {
|
|||
async move { clone.backend.db().get_recordings_for_work(&clone.work.id).await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(|recording| {
|
||||
let label = gtk::Label::new(Some(&recording.get_performers()));
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_margin_start(6);
|
||||
label.set_margin_end(6);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
label.upcast()
|
||||
});
|
||||
this.selector.set_make_widget(clone!(@strong this => move |recording| {
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&recording.get_performers()));
|
||||
|
||||
let recording = recording.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
this.select(&recording);
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector.set_filter(|search, recording| {
|
||||
recording.get_performers().to_lowercase().contains(search)
|
||||
});
|
||||
|
||||
this.selector
|
||||
.set_selected_cb(clone!(@strong this => move |recording| this.select(recording)));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use crate::widgets::List;
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::HeaderBarExt;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
|
@ -14,12 +12,16 @@ use std::rc::Rc;
|
|||
/// database and to search within the list.
|
||||
pub struct Selector<T: 'static> {
|
||||
pub widget: gtk::Box,
|
||||
header: libhandy::HeaderBar,
|
||||
title_label: gtk::Label,
|
||||
subtitle_label: gtk::Label,
|
||||
search_entry: gtk::SearchEntry,
|
||||
server_check_button: gtk::CheckButton,
|
||||
stack: gtk::Stack,
|
||||
list: Rc<List<T>>,
|
||||
list: Rc<List>,
|
||||
items: RefCell<Vec<T>>,
|
||||
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
|
||||
load_online: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Result<Vec<T>>>>>>>,
|
||||
load_local: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Vec<T>>>>>>,
|
||||
filter: RefCell<Option<Box<dyn Fn(&str, &T) -> bool>>>,
|
||||
|
|
@ -33,7 +35,8 @@ impl<T> Selector<T> {
|
|||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/selector.ui");
|
||||
|
||||
get_widget!(builder, gtk::Box, widget);
|
||||
get_widget!(builder, libhandy::HeaderBar, header);
|
||||
get_widget!(builder, gtk::Label, title_label);
|
||||
get_widget!(builder, gtk::Label, subtitle_label);
|
||||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Button, add_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
|
|
@ -42,17 +45,21 @@ impl<T> Selector<T> {
|
|||
get_widget!(builder, gtk::Frame, frame);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
|
||||
let list = List::<T>::new(&gettext("Nothing found."));
|
||||
frame.add(&list.widget);
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
header,
|
||||
title_label,
|
||||
subtitle_label,
|
||||
search_entry,
|
||||
server_check_button,
|
||||
stack,
|
||||
list,
|
||||
items: RefCell::new(Vec::new()),
|
||||
back_cb: RefCell::new(None),
|
||||
add_cb: RefCell::new(None),
|
||||
make_widget: RefCell::new(None),
|
||||
load_online: RefCell::new(None),
|
||||
load_local: RefCell::new(None),
|
||||
filter: RefCell::new(None),
|
||||
|
|
@ -72,7 +79,7 @@ impl<T> Selector<T> {
|
|||
}
|
||||
}));
|
||||
|
||||
search_entry.connect_search_changed(clone!(@strong this => move |_| {
|
||||
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
|
||||
this.list.invalidate_filter();
|
||||
}));
|
||||
|
||||
|
|
@ -85,17 +92,25 @@ impl<T> Selector<T> {
|
|||
}
|
||||
}));
|
||||
|
||||
this.list.set_filter(
|
||||
clone!(@strong this, @strong search_entry => move |item: &T| {
|
||||
match &*this.filter.borrow() {
|
||||
Some(filter) => {
|
||||
let search = search_entry.get_text().to_string().to_lowercase();
|
||||
search.is_empty() || filter(&search, item)
|
||||
}
|
||||
None => true,
|
||||
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
if let Some(cb) = &*this.make_widget.borrow() {
|
||||
let item = &this.items.borrow()[index];
|
||||
cb(item)
|
||||
} else {
|
||||
gtk::Label::new(None).upcast()
|
||||
}
|
||||
}));
|
||||
|
||||
this.list.set_filter_cb(clone!(@strong this => move |index| {
|
||||
match &*this.filter.borrow() {
|
||||
Some(filter) => {
|
||||
let item = &this.items.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
search.is_empty() || filter(&search, item)
|
||||
}
|
||||
}),
|
||||
);
|
||||
None => true,
|
||||
}
|
||||
}));
|
||||
|
||||
try_again_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
this.clone().load_online();
|
||||
|
|
@ -109,12 +124,13 @@ impl<T> Selector<T> {
|
|||
|
||||
/// Set the title to be shown in the header.
|
||||
pub fn set_title(&self, title: &str) {
|
||||
self.header.set_title(Some(title));
|
||||
self.title_label.set_label(&title);
|
||||
}
|
||||
|
||||
/// Set the subtitle to be shown in the header.
|
||||
pub fn set_subtitle(&self, subtitle: &str) {
|
||||
self.header.set_subtitle(Some(subtitle));
|
||||
self.subtitle_label.set_label(&subtitle);
|
||||
self.subtitle_label.show();
|
||||
}
|
||||
|
||||
/// Set the closure to be called when the user wants to go back.
|
||||
|
|
@ -150,7 +166,7 @@ impl<T> Selector<T> {
|
|||
|
||||
/// Set the closure to be called for creating a new list row.
|
||||
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
||||
self.list.set_make_widget(make_widget);
|
||||
self.make_widget.replace(Some(Box::new(make_widget)));
|
||||
}
|
||||
|
||||
/// Set a closure to call when deciding whether to show an item based on a search string. The
|
||||
|
|
@ -159,11 +175,6 @@ impl<T> Selector<T> {
|
|||
self.filter.replace(Some(Box::new(filter)));
|
||||
}
|
||||
|
||||
/// Set the closure to be called when an item is selected.
|
||||
pub fn set_selected_cb<F: Fn(&T) -> () + 'static>(&self, cb: F) {
|
||||
self.list.set_selected(cb);
|
||||
}
|
||||
|
||||
fn load_online(self: Rc<Self>) {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = self.clone();
|
||||
|
|
@ -173,11 +184,10 @@ impl<T> Selector<T> {
|
|||
|
||||
match Pin::from(cb()).await {
|
||||
Ok(items) => {
|
||||
clone.list.show_items(items);
|
||||
clone.stack.set_visible_child_name("content");
|
||||
clone.show_items(items);
|
||||
}
|
||||
Err(_) => {
|
||||
clone.list.show_items(Vec::new());
|
||||
clone.show_items(Vec::new());
|
||||
clone.stack.set_visible_child_name("error");
|
||||
}
|
||||
}
|
||||
|
|
@ -193,9 +203,15 @@ impl<T> Selector<T> {
|
|||
self.stack.set_visible_child_name("loading");
|
||||
|
||||
let items = Pin::from(cb()).await;
|
||||
clone.list.show_items(items);
|
||||
clone.stack.set_visible_child_name("content");
|
||||
clone.show_items(items);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_items(&self, items: Vec<T>) {
|
||||
let length = items.len();
|
||||
self.items.replace(items);
|
||||
self.list.update(length);
|
||||
self.stack.set_visible_child_name("content");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::widgets::{Navigator, NavigatorScreen};
|
|||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
|
@ -73,22 +74,22 @@ impl WorkSelector {
|
|||
async move { clone.backend.db().get_works(&clone.person.id).await.unwrap() }
|
||||
}));
|
||||
|
||||
this.selector.set_make_widget(|work| {
|
||||
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()
|
||||
});
|
||||
this.selector.set_make_widget(clone!(@strong this => move |work| {
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&work.title));
|
||||
|
||||
let work = work.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
this.select(&work);
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_filter(|search, work| work.title.to_lowercase().contains(search));
|
||||
|
||||
this.selector
|
||||
.set_selected_cb(clone!(@strong this => move |work| this.select(work)));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
|
|
|
|||
189
src/widgets/indexed_list_model.rs
Normal file
189
src/widgets/indexed_list_model.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
use glib::prelude::*;
|
||||
use glib::subclass;
|
||||
use glib::subclass::prelude::*;
|
||||
use gio::prelude::*;
|
||||
use gio::subclass::prelude::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct IndexedListModel(ObjectSubclass<indexed_list_model::IndexedListModel>)
|
||||
@implements gio::ListModel;
|
||||
}
|
||||
|
||||
impl IndexedListModel {
|
||||
/// Create a new indexed list model, which will be empty initially.
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[]).unwrap()
|
||||
}
|
||||
|
||||
/// Set the length of the list model.
|
||||
pub fn set_length(&self, length: u32) {
|
||||
let old_length = self.get_property("length").unwrap().get_some::<u32>().unwrap();
|
||||
self.set_property("length", &length).unwrap();
|
||||
self.items_changed(0, old_length, length);
|
||||
}
|
||||
}
|
||||
|
||||
mod indexed_list_model {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IndexedListModel {
|
||||
length: Cell<u32>,
|
||||
}
|
||||
|
||||
static PROPERTIES: [subclass::Property; 1] = [
|
||||
subclass::Property("length", |length| {
|
||||
glib::ParamSpec::uint(
|
||||
length,
|
||||
"Length",
|
||||
"Length",
|
||||
0,
|
||||
std::u32::MAX,
|
||||
0,
|
||||
glib::ParamFlags::READWRITE,
|
||||
)
|
||||
}),
|
||||
];
|
||||
|
||||
impl ObjectSubclass for IndexedListModel {
|
||||
const NAME: &'static str = "IndexedListModel";
|
||||
|
||||
type Type = super::IndexedListModel;
|
||||
type ParentType = glib::Object;
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib::object_subclass!();
|
||||
|
||||
fn type_init(type_: &mut subclass::InitializingType<Self>) {
|
||||
type_.add_interface::<gio::ListModel>();
|
||||
}
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.install_properties(&PROPERTIES);
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
Self { length: Cell::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for IndexedListModel {
|
||||
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value) {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("length", ..) => {
|
||||
let length = value.get().unwrap().unwrap();
|
||||
self.length.set(length);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("length", ..) => self.length.get().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListModelImpl for IndexedListModel {
|
||||
fn get_item_type(&self, _: &Self::Type) -> glib::Type {
|
||||
ItemIndex::static_type()
|
||||
}
|
||||
|
||||
fn get_n_items(&self, _: &Self::Type) -> u32 {
|
||||
self.length.get()
|
||||
}
|
||||
|
||||
fn get_item(&self, _: &Self::Type, position: u32) -> Option<glib::Object> {
|
||||
Some(ItemIndex::new(position).upcast())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ItemIndex(ObjectSubclass<item_index::ItemIndex>);
|
||||
}
|
||||
|
||||
impl ItemIndex {
|
||||
/// Create a new item index.
|
||||
pub fn new(value: u32) -> Self {
|
||||
glib::Object::new(&[("value", &value)]).unwrap()
|
||||
}
|
||||
|
||||
/// Get the value of the item index..
|
||||
pub fn get(&self) -> u32 {
|
||||
self.get_property("value").unwrap().get_some::<u32>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
mod item_index {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ItemIndex {
|
||||
value: Cell<u32>,
|
||||
}
|
||||
|
||||
static PROPERTIES: [subclass::Property; 1] = [
|
||||
subclass::Property("value", |value| {
|
||||
glib::ParamSpec::uint(
|
||||
value,
|
||||
"Value",
|
||||
"Value",
|
||||
0,
|
||||
std::u32::MAX,
|
||||
0,
|
||||
glib::ParamFlags::READWRITE,
|
||||
)
|
||||
}),
|
||||
];
|
||||
|
||||
impl ObjectSubclass for ItemIndex {
|
||||
const NAME: &'static str = "ItemIndex";
|
||||
|
||||
type Type = super::ItemIndex;
|
||||
type ParentType = glib::Object;
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib::object_subclass!();
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.install_properties(&PROPERTIES);
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
Self { value: Cell::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ItemIndex {
|
||||
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value) {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("value", ..) => {
|
||||
let value = value.get().unwrap().unwrap();
|
||||
self.value.set(value);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("value", ..) => self.value.get().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +1,88 @@
|
|||
use super::*;
|
||||
use super::indexed_list_model::{IndexedListModel, ItemIndex};
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::convert::TryInto;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct List<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
/// A simple list of widgets.
|
||||
pub struct List {
|
||||
pub widget: gtk::ListBox,
|
||||
items: RefCell<Vec<T>>,
|
||||
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
|
||||
filter: RefCell<Option<Box<dyn Fn(&T) -> bool>>>,
|
||||
selected: RefCell<Option<Box<dyn Fn(&T) -> ()>>>,
|
||||
model: IndexedListModel,
|
||||
filter: gtk::CustomFilter,
|
||||
make_widget_cb: RefCell<Option<Box<dyn Fn(usize) -> gtk::Widget>>>,
|
||||
filter_cb: RefCell<Option<Box<dyn Fn(usize) -> bool>>>,
|
||||
}
|
||||
|
||||
impl<T> List<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
pub fn new(placeholder_text: &str) -> Rc<Self> {
|
||||
let placeholder_label = gtk::Label::new(Some(placeholder_text));
|
||||
placeholder_label.set_margin_top(6);
|
||||
placeholder_label.set_margin_bottom(6);
|
||||
placeholder_label.set_margin_start(6);
|
||||
placeholder_label.set_margin_end(6);
|
||||
placeholder_label.show();
|
||||
impl List {
|
||||
/// Create a new list. The list will be empty initially.
|
||||
pub fn new() -> Rc<Self> {
|
||||
let model = IndexedListModel::new();
|
||||
let filter = gtk::CustomFilter::new(|_| true);
|
||||
let filter_model = gtk::FilterListModel::new(Some(&model), Some(&filter));
|
||||
|
||||
// TODO: Switch to gtk::ListView.
|
||||
// let selection = gtk::NoSelection::new(Some(&model));
|
||||
// let factory = gtk::SignalListItemFactory::new();
|
||||
// let widget = gtk::ListView::new(Some(&selection), Some(&factory));
|
||||
|
||||
let widget = gtk::ListBox::new();
|
||||
widget.set_placeholder(Some(&placeholder_label));
|
||||
widget.show();
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
items: RefCell::new(Vec::new()),
|
||||
make_widget: RefCell::new(None),
|
||||
filter: RefCell::new(None),
|
||||
selected: RefCell::new(None),
|
||||
model,
|
||||
filter,
|
||||
make_widget_cb: RefCell::new(None),
|
||||
filter_cb: RefCell::new(None),
|
||||
});
|
||||
|
||||
this.widget
|
||||
.connect_row_activated(clone!(@strong this => move |_, row| {
|
||||
if let Some(selected) = &*this.selected.borrow() {
|
||||
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
|
||||
let index: usize = row.get_index().try_into().unwrap();
|
||||
selected(&this.items.borrow()[index]);
|
||||
}
|
||||
}));
|
||||
this.filter.set_filter_func(clone!(@strong this => move |index| {
|
||||
if let Some(cb) = &*this.filter_cb.borrow() {
|
||||
let index = index.downcast_ref::<ItemIndex>().unwrap().get() as usize;
|
||||
cb(index)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}));
|
||||
|
||||
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::<SelectorRow>().unwrap();
|
||||
let index: usize = row.get_index().try_into().unwrap();
|
||||
filter(&this.items.borrow()[index])
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}))));
|
||||
this.widget.bind_model(Some(&filter_model), clone!(@strong this => move |index| {
|
||||
let index = index.downcast_ref::<ItemIndex>().unwrap().get() as usize;
|
||||
if let Some(cb) = &*this.make_widget_cb.borrow() {
|
||||
cb(index)
|
||||
} else {
|
||||
gtk::Label::new(None).upcast()
|
||||
}
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn set_selectable(&self, selectable: bool) {
|
||||
let mode = if selectable {
|
||||
gtk::SelectionMode::Single
|
||||
} else {
|
||||
gtk::SelectionMode::None
|
||||
};
|
||||
|
||||
self.widget.set_selection_mode(mode);
|
||||
/// Set the closure to be called to construct widgets for the items.
|
||||
pub fn set_make_widget_cb<F: Fn(usize) -> gtk::Widget + 'static>(&self, cb: F) {
|
||||
self.make_widget_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
||||
self.make_widget.replace(Some(Box::new(make_widget)));
|
||||
/// Set the closure to be called to filter the items. If this returns
|
||||
/// false, the item will not be shown.
|
||||
pub fn set_filter_cb<F: Fn(usize) -> bool + 'static>(&self, cb: F) {
|
||||
self.filter_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
pub fn get_selected_index(&self) -> Option<usize> {
|
||||
match self.widget.get_selected_rows().first() {
|
||||
Some(row) => match row.get_child() {
|
||||
Some(child) => Some(
|
||||
child
|
||||
.downcast::<SelectorRow>()
|
||||
.unwrap()
|
||||
.get_index()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
),
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
/// Select an item by its index. If the index is out of range, nothing will happen.
|
||||
pub fn select(&self, index: usize) {
|
||||
let row = self.widget.get_row_at_index(index as i32);
|
||||
if let Some(row) = row {
|
||||
self.widget.select_row(Some(&row));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_index(&self, index: usize) {
|
||||
self.widget.select_row(
|
||||
self.widget
|
||||
.get_row_at_index(index.try_into().unwrap())
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn show_items(&self, items: Vec<T>) {
|
||||
self.items.replace(items);
|
||||
self.update();
|
||||
}
|
||||
|
||||
/// Refilter the list based on the filter callback.
|
||||
pub fn invalidate_filter(&self) {
|
||||
self.widget.invalidate_filter();
|
||||
self.filter.changed(gtk::FilterChange::Different);
|
||||
}
|
||||
|
||||
pub fn update(&self) {
|
||||
for child in self.widget.get_children() {
|
||||
self.widget.remove(&child);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_selection(&self) {
|
||||
self.widget.unselect_all();
|
||||
/// Call the make_widget function for each item. This will automatically
|
||||
/// show all children by indices 0..length.
|
||||
pub fn update(&self, length: usize) {
|
||||
self.model.set_length(length as u32);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,10 @@ pub use navigator::*;
|
|||
pub mod navigator_window;
|
||||
pub use navigator_window::*;
|
||||
|
||||
pub mod new_list;
|
||||
|
||||
pub mod player_bar;
|
||||
pub use player_bar::*;
|
||||
|
||||
pub mod poe_list;
|
||||
pub use poe_list::*;
|
||||
|
||||
pub mod selector_row;
|
||||
pub use selector_row::*;
|
||||
mod indexed_list_model;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ impl Navigator {
|
|||
widget.set_interpolate_size(true);
|
||||
widget.set_transition_type(gtk::StackTransitionType::Crossfade);
|
||||
widget.set_hexpand(true);
|
||||
widget.add_named(empty_screen, "empty_screen");
|
||||
widget.add_named(empty_screen, Some("empty_screen"));
|
||||
widget.show();
|
||||
|
||||
let result = Rc::new(Self {
|
||||
|
|
@ -70,7 +70,7 @@ impl Navigator {
|
|||
}
|
||||
|
||||
let widget = screen.get_widget();
|
||||
self.widget.add(&widget);
|
||||
self.widget.add_child(&widget);
|
||||
self.widget.set_visible_child(&widget);
|
||||
|
||||
screen.attach_navigator(self.clone());
|
||||
|
|
@ -116,7 +116,7 @@ impl Navigator {
|
|||
}
|
||||
|
||||
let widget = screen.get_widget();
|
||||
self.widget.add(&widget);
|
||||
self.widget.add_child(&widget);
|
||||
self.widget.set_visible_child(&widget);
|
||||
|
||||
screen.attach_navigator(self.clone());
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl NavigatorWindow {
|
|||
window.set_default_size(600, 424);
|
||||
let placeholder = gtk::Label::new(None);
|
||||
let navigator = Navigator::new(&window, &placeholder);
|
||||
window.add(&navigator.widget);
|
||||
libhandy::WindowExt::set_child(&window, Some(&navigator.widget));
|
||||
|
||||
let this = Rc::new(Self { window, navigator });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
use gtk::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// A simple list of widgets.
|
||||
pub struct List {
|
||||
pub widget: gtk::ListBox,
|
||||
make_widget: RefCell<Option<Box<dyn Fn(usize) -> gtk::Widget>>>,
|
||||
}
|
||||
|
||||
impl List {
|
||||
/// Create a new list. The list will be empty.
|
||||
pub fn new(placeholder_text: &str) -> Self {
|
||||
let placeholder_label = gtk::Label::new(Some(placeholder_text));
|
||||
placeholder_label.set_margin_top(6);
|
||||
placeholder_label.set_margin_bottom(6);
|
||||
placeholder_label.set_margin_start(6);
|
||||
placeholder_label.set_margin_end(6);
|
||||
placeholder_label.show();
|
||||
|
||||
let widget = gtk::ListBox::new();
|
||||
widget.set_selection_mode(gtk::SelectionMode::None);
|
||||
widget.set_placeholder(Some(&placeholder_label));
|
||||
widget.show();
|
||||
|
||||
Self {
|
||||
widget,
|
||||
make_widget: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the closure to be called to construct widgets for the items.
|
||||
pub fn set_make_widget<F: Fn(usize) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
||||
self.make_widget.replace(Some(Box::new(make_widget)));
|
||||
}
|
||||
|
||||
/// Call the make_widget function for each item. This will automatically
|
||||
/// show all children by indices 0..length.
|
||||
pub fn update(&self, length: usize) {
|
||||
for child in self.widget.get_children() {
|
||||
self.widget.remove(&child);
|
||||
}
|
||||
|
||||
if let Some(make_widget) = &*self.make_widget.borrow() {
|
||||
for index in 0..length {
|
||||
let row = make_widget(index);
|
||||
self.widget.insert(&row, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -144,15 +144,11 @@ impl PlayerBar {
|
|||
@strong self.play_image as play_image,
|
||||
@strong self.pause_image as pause_image
|
||||
=> move |playing| {
|
||||
if let Some(child) = play_button.get_child() {
|
||||
play_button.remove( &child);
|
||||
}
|
||||
|
||||
play_button.add(if playing {
|
||||
play_button.set_child(Some(if playing {
|
||||
&pause_image
|
||||
} else {
|
||||
&play_image
|
||||
});
|
||||
}));
|
||||
}
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use super::*;
|
||||
use crate::backend::Backend;
|
||||
use crate::database::*;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use libhandy::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -24,9 +25,12 @@ impl PersonOrEnsemble {
|
|||
|
||||
pub struct PoeList {
|
||||
pub widget: gtk::Box,
|
||||
list: Rc<List<PersonOrEnsemble>>,
|
||||
backend: Rc<Backend>,
|
||||
stack: gtk::Stack,
|
||||
search_entry: gtk::SearchEntry,
|
||||
list: Rc<List>,
|
||||
data: RefCell<Vec<PersonOrEnsemble>>,
|
||||
selected_cb: RefCell<Option<Box<dyn Fn(&PersonOrEnsemble)>>>,
|
||||
}
|
||||
|
||||
impl PoeList {
|
||||
|
|
@ -38,47 +42,54 @@ impl PoeList {
|
|||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::ScrolledWindow, scrolled_window);
|
||||
|
||||
let list = List::new(&gettext("No persons or ensembles found."));
|
||||
let list = List::new();
|
||||
list.widget.add_css_class("navigation-sidebar");
|
||||
|
||||
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()
|
||||
});
|
||||
scrolled_window.set_child(Some(&list.widget));
|
||||
|
||||
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)
|
||||
}),
|
||||
);
|
||||
|
||||
scrolled_window.add(&list.widget);
|
||||
|
||||
let result = Rc::new(Self {
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
list,
|
||||
backend,
|
||||
stack,
|
||||
search_entry,
|
||||
list,
|
||||
data: RefCell::new(Vec::new()),
|
||||
selected_cb: RefCell::new(None),
|
||||
});
|
||||
|
||||
search_entry.connect_search_changed(clone!(@strong result => move |_| {
|
||||
result.list.invalidate_filter();
|
||||
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
|
||||
this.list.invalidate_filter();
|
||||
}));
|
||||
|
||||
result
|
||||
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
let poe = &this.data.borrow()[index];
|
||||
|
||||
let row = libhandy::ActionRow::new();
|
||||
row.set_activatable(true);
|
||||
row.set_title(Some(&poe.get_title()));
|
||||
|
||||
let poe = poe.to_owned();
|
||||
row.connect_activated(clone!(@strong this => move |_| {
|
||||
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||
cb(&poe);
|
||||
}
|
||||
}));
|
||||
|
||||
row.upcast()
|
||||
}));
|
||||
|
||||
this.list.set_filter_cb(clone!(@strong this => move |index| {
|
||||
let poe = &this.data.borrow()[index];
|
||||
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
|
||||
let title = poe.get_title().to_lowercase();
|
||||
search.is_empty() || title.contains(&search)
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn set_selected<S>(&self, selected: S)
|
||||
where
|
||||
S: Fn(&PersonOrEnsemble) -> () + 'static,
|
||||
{
|
||||
self.list.set_selected(selected);
|
||||
pub fn set_selected_cb<F: Fn(&PersonOrEnsemble) + 'static>(&self, cb: F) {
|
||||
self.selected_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
pub fn reload(self: Rc<Self>) {
|
||||
|
|
@ -86,7 +97,6 @@ impl PoeList {
|
|||
|
||||
let context = glib::MainContext::default();
|
||||
let backend = self.backend.clone();
|
||||
let list = self.list.clone();
|
||||
|
||||
context.spawn_local(async move {
|
||||
let persons = backend.db().get_persons().await.unwrap();
|
||||
|
|
@ -101,7 +111,9 @@ impl PoeList {
|
|||
poes.push(PersonOrEnsemble::Ensemble(ensemble));
|
||||
}
|
||||
|
||||
list.show_items(poes);
|
||||
let length = poes.len();
|
||||
self.data.replace(poes);
|
||||
self.list.update(length);
|
||||
|
||||
self.stack.set_visible_child_name("content");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,149 +0,0 @@
|
|||
use glib::prelude::*;
|
||||
use glib::subclass;
|
||||
use glib::subclass::prelude::*;
|
||||
use glib::translate::*;
|
||||
use glib::{glib_object_impl, glib_object_subclass, glib_wrapper};
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
glib_wrapper! {
|
||||
pub struct SelectorRow(
|
||||
Object<subclass::simple::InstanceStruct<SelectorRowPriv>,
|
||||
subclass::simple::ClassStruct<SelectorRowPriv>,
|
||||
SelectorRowClass>
|
||||
) @extends gtk::Bin, gtk::Container, gtk::Widget;
|
||||
|
||||
match fn {
|
||||
get_type => || SelectorRowPriv::get_type().to_glib(),
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectorRow {
|
||||
pub fn new<T: IsA<gtk::Widget>>(index: u64, child: &T) -> Self {
|
||||
glib::Object::new(
|
||||
Self::static_type(),
|
||||
&[("index", &index), ("child", child.upcast_ref())],
|
||||
)
|
||||
.expect("Failed to create SelectorRow GObject!")
|
||||
.downcast()
|
||||
.expect("SelectorRow GObject is of the wrong type!")
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> u64 {
|
||||
self.get_property("index").unwrap().get().unwrap().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SelectorRowPriv {
|
||||
index: Cell<u64>,
|
||||
child: RefCell<Option<gtk::Widget>>,
|
||||
}
|
||||
|
||||
static PROPERTIES: [subclass::Property; 2] = [
|
||||
subclass::Property("index", |name| {
|
||||
glib::ParamSpec::uint64(
|
||||
name,
|
||||
"Index",
|
||||
"Index",
|
||||
0,
|
||||
u64::MAX,
|
||||
0,
|
||||
glib::ParamFlags::READWRITE,
|
||||
)
|
||||
}),
|
||||
subclass::Property("child", |name| {
|
||||
glib::ParamSpec::object(
|
||||
name,
|
||||
"Child",
|
||||
"Child",
|
||||
gtk::Widget::static_type(),
|
||||
glib::ParamFlags::READWRITE,
|
||||
)
|
||||
}),
|
||||
];
|
||||
|
||||
impl ObjectSubclass for SelectorRowPriv {
|
||||
const NAME: &'static str = "SelectorRow";
|
||||
type ParentType = gtk::Bin;
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib_object_subclass!();
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.install_properties(&PROPERTIES);
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
index: Cell::new(0),
|
||||
child: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for SelectorRowPriv {
|
||||
glib_object_impl!();
|
||||
|
||||
fn constructed(&self, object: &glib::Object) {
|
||||
self.parent_constructed(object);
|
||||
|
||||
let row = object.downcast_ref::<SelectorRow>().unwrap();
|
||||
|
||||
let child = self.child.borrow();
|
||||
match child.as_ref() {
|
||||
Some(child) => row.add(child),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_property(&self, object: &glib::Object, id: usize, value: &glib::Value) {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("index", ..) => {
|
||||
let index = value
|
||||
.get_some()
|
||||
.expect("Wrong type for SelectorRow GObject index property!");
|
||||
self.index.set(index);
|
||||
}
|
||||
subclass::Property("child", ..) => {
|
||||
let child = value
|
||||
.get()
|
||||
.expect("Wrong type for SelectorRow GObject child property!");
|
||||
|
||||
let row = object.downcast_ref::<SelectorRow>().unwrap();
|
||||
|
||||
{
|
||||
let old = self.child.borrow();
|
||||
match old.as_ref() {
|
||||
Some(old) => row.remove(old),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
self.child.replace(child.clone());
|
||||
match child {
|
||||
Some(child) => row.add(&child),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("index", ..) => Ok(self.index.get().to_value()),
|
||||
subclass::Property("child", ..) => Ok(self.child.borrow().to_value()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for SelectorRowPriv {}
|
||||
impl ContainerImpl for SelectorRowPriv {}
|
||||
impl BinImpl for SelectorRowPriv {}
|
||||
|
|
@ -9,7 +9,6 @@ use gio::prelude::*;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::{action, get_widget};
|
||||
use libhandy::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct Window {
|
||||
|
|
@ -21,7 +20,7 @@ pub struct Window {
|
|||
poe_list: Rc<PoeList>,
|
||||
navigator: Rc<Navigator>,
|
||||
player_bar: PlayerBar,
|
||||
player_screen: PlayerScreen,
|
||||
player_screen: Rc<PlayerScreen>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -40,7 +39,7 @@ impl Window {
|
|||
let backend = Rc::new(Backend::new());
|
||||
|
||||
let player_screen = PlayerScreen::new();
|
||||
stack.add_named(&player_screen.widget, "player_screen");
|
||||
stack.add_named(&player_screen.widget, Some("player_screen"));
|
||||
|
||||
let poe_list = PoeList::new(backend.clone());
|
||||
let navigator = Navigator::new(&window, &empty_screen);
|
||||
|
|
@ -49,7 +48,7 @@ impl Window {
|
|||
}));
|
||||
|
||||
let player_bar = PlayerBar::new();
|
||||
content_box.add(&player_bar.widget);
|
||||
content_box.append(&player_bar.widget);
|
||||
|
||||
let result = Rc::new(Self {
|
||||
backend,
|
||||
|
|
@ -73,15 +72,21 @@ impl Window {
|
|||
None,
|
||||
None);
|
||||
|
||||
if let gtk::ResponseType::Accept = dialog.run() {
|
||||
if let Some(path) = dialog.get_filename() {
|
||||
let context = glib::MainContext::default();
|
||||
let backend = result.backend.clone();
|
||||
context.spawn_local(async move {
|
||||
backend.set_music_library_path(path).await.unwrap();
|
||||
});
|
||||
dialog.connect_response(clone!(@strong result => move |dialog, response| {
|
||||
if response == gtk::ResponseType::Accept {
|
||||
if let Some(file) = dialog.get_file() {
|
||||
if let Some(path) = file.get_path() {
|
||||
let context = glib::MainContext::default();
|
||||
let backend = result.backend.clone();
|
||||
context.spawn_local(async move {
|
||||
backend.set_music_library_path(path).await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
dialog.show();
|
||||
}));
|
||||
|
||||
add_button.connect_clicked(clone!(@strong result => move |_| {
|
||||
|
|
@ -156,7 +161,7 @@ impl Window {
|
|||
|
||||
let player = clone.backend.get_player().unwrap();
|
||||
clone.player_bar.set_player(Some(player.clone()));
|
||||
clone.player_screen.set_player(Some(player));
|
||||
clone.player_screen.clone().set_player(Some(player));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -169,11 +174,11 @@ impl Window {
|
|||
clone.backend.clone().init().await.unwrap();
|
||||
});
|
||||
|
||||
result.leaflet.add(&result.navigator.widget);
|
||||
result.leaflet.append(&result.navigator.widget);
|
||||
|
||||
result
|
||||
.poe_list
|
||||
.set_selected(clone!(@strong result => move |poe| {
|
||||
.set_selected_cb(clone!(@strong result => move |poe| {
|
||||
result.leaflet.set_visible_child(&result.navigator.widget);
|
||||
match poe {
|
||||
PersonOrEnsemble::Person(person) => {
|
||||
|
|
@ -187,7 +192,7 @@ impl Window {
|
|||
|
||||
result
|
||||
.sidebar_box
|
||||
.pack_start(&result.poe_list.widget, true, true, 0);
|
||||
.append(&result.poe_list.widget);
|
||||
|
||||
result
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue