Initial port to GTK4

This commit is contained in:
Elias Projahn 2021-01-25 14:00:57 +01:00
parent 1a9e58d627
commit 801a130ef8
76 changed files with 3098 additions and 6625 deletions

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
}
}