mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Replace old track editors with import dialogs
This commit is contained in:
parent
c7928003e4
commit
4aa858602d
16 changed files with 856 additions and 552 deletions
69
musicus/src/dialogs/import.rs
Normal file
69
musicus/src/dialogs/import.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::editors::{TrackSetEditor, TrackSource};
|
||||||
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A dialog for editing metadata while importing music into the music library.
|
||||||
|
pub struct ImportDialog {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
source: Rc<TrackSource>,
|
||||||
|
widget: gtk::Box,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportDialog {
|
||||||
|
/// Create a new import dialog.
|
||||||
|
pub fn new(backend: Rc<Backend>, source: Rc<TrackSource>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_dialog.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, add_button);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
source,
|
||||||
|
widget,
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
add_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone());
|
||||||
|
navigator.push(editor);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for ImportDialog {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
musicus/src/dialogs/import_folder.rs
Normal file
88
musicus/src/dialogs/import_folder.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
use super::ImportDialog;
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::editors::TrackSource;
|
||||||
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk_macros::get_widget;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// The initial screen for importing a folder.
|
||||||
|
pub struct ImportFolderDialog {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
widget: gtk::Box,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportFolderDialog {
|
||||||
|
/// Create a new import folderdialog.
|
||||||
|
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_folder_dialog.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, import_button);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
widget,
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let chooser = gtk::FileChooserNative::new(
|
||||||
|
Some("Select folder"),
|
||||||
|
Some(&navigator.window),
|
||||||
|
gtk::FileChooserAction::SelectFolder,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
chooser.connect_response(clone!(@strong this => move |chooser, response| {
|
||||||
|
if response == gtk::ResponseType::Accept {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let path = chooser.get_filename().unwrap();
|
||||||
|
let source = TrackSource::folder(&path).unwrap();
|
||||||
|
let dialog = ImportDialog::new(this.backend.clone(), Rc::new(source));
|
||||||
|
navigator.push(dialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
chooser.run();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for ImportFolderDialog {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
pub mod import;
|
||||||
|
pub use import::*;
|
||||||
|
|
||||||
|
pub mod import_folder;
|
||||||
|
pub use import_folder::*;
|
||||||
|
|
||||||
pub mod import_disc;
|
pub mod import_disc;
|
||||||
pub use import_disc::*;
|
pub use import_disc::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,15 @@ pub use person::*;
|
||||||
pub mod recording;
|
pub mod recording;
|
||||||
pub use recording::*;
|
pub use recording::*;
|
||||||
|
|
||||||
pub mod tracks;
|
pub mod track_set;
|
||||||
pub use tracks::*;
|
pub use track_set::*;
|
||||||
|
|
||||||
|
pub mod track_source;
|
||||||
|
pub use track_source::*;
|
||||||
|
|
||||||
pub mod work;
|
pub mod work;
|
||||||
pub use work::*;
|
pub use work::*;
|
||||||
|
|
||||||
mod performance;
|
mod performance;
|
||||||
mod track;
|
|
||||||
mod work_part;
|
mod work_part;
|
||||||
mod work_section;
|
mod work_section;
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
use crate::database::*;
|
|
||||||
use crate::widgets::{Navigator, NavigatorScreen};
|
|
||||||
use glib::clone;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk_macros::get_widget;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// A screen for editing a single track.
|
|
||||||
// TODO: Refactor.
|
|
||||||
pub struct TrackEditor {
|
|
||||||
widget: gtk::Box,
|
|
||||||
ready_cb: RefCell<Option<Box<dyn Fn(Track) -> ()>>>,
|
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrackEditor {
|
|
||||||
/// Create a new track editor.
|
|
||||||
pub fn new(track: Track, work: Work) -> Rc<Self> {
|
|
||||||
// Create UI
|
|
||||||
|
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
|
|
||||||
|
|
||||||
get_widget!(builder, gtk::Box, widget);
|
|
||||||
get_widget!(builder, gtk::Button, back_button);
|
|
||||||
get_widget!(builder, gtk::Button, save_button);
|
|
||||||
get_widget!(builder, gtk::ListBox, list);
|
|
||||||
|
|
||||||
let this = Rc::new(Self {
|
|
||||||
widget,
|
|
||||||
ready_cb: RefCell::new(None),
|
|
||||||
navigator: RefCell::new(None),
|
|
||||||
});
|
|
||||||
|
|
||||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
let work = Rc::new(work);
|
|
||||||
let work_parts = Rc::new(RefCell::new(track.work_parts));
|
|
||||||
let file_name = track.file_name;
|
|
||||||
|
|
||||||
save_button.connect_clicked(clone!(@strong this, @strong work_parts => move |_| {
|
|
||||||
let mut work_parts = work_parts.borrow_mut();
|
|
||||||
work_parts.sort();
|
|
||||||
|
|
||||||
if let Some(cb) = &*this.ready_cb.borrow() {
|
|
||||||
cb(Track {
|
|
||||||
work_parts: work_parts.clone(),
|
|
||||||
file_name: file_name.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (index, part) in work.parts.iter().enumerate() {
|
|
||||||
let check = gtk::CheckButton::new();
|
|
||||||
check.set_active(work_parts.borrow().contains(&index));
|
|
||||||
check.connect_toggled(clone!(@strong check, @strong work_parts => move |_| {
|
|
||||||
if check.get_active() {
|
|
||||||
let mut work_parts = work_parts.borrow_mut();
|
|
||||||
work_parts.push(index);
|
|
||||||
} else {
|
|
||||||
let mut work_parts = work_parts.borrow_mut();
|
|
||||||
if let Some(pos) = work_parts.iter().position(|part| *part == index) {
|
|
||||||
work_parts.remove(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
let label = gtk::Label::new(Some(&part.title));
|
|
||||||
label.set_halign(gtk::Align::Start);
|
|
||||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
|
|
||||||
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
|
||||||
hbox.set_border_width(6);
|
|
||||||
hbox.add(&check);
|
|
||||||
hbox.add(&label);
|
|
||||||
|
|
||||||
let row = gtk::ListBoxRow::new();
|
|
||||||
row.add(&hbox);
|
|
||||||
row.show_all();
|
|
||||||
|
|
||||||
list.add(&row);
|
|
||||||
list.connect_row_activated(
|
|
||||||
clone!(@strong row, @strong check => move |_, activated_row| {
|
|
||||||
if *activated_row == row {
|
|
||||||
check.activate();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut section_count = 0;
|
|
||||||
for section in &work.sections {
|
|
||||||
let attributes = pango::AttrList::new();
|
|
||||||
attributes.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap());
|
|
||||||
|
|
||||||
let label = gtk::Label::new(Some(§ion.title));
|
|
||||||
label.set_halign(gtk::Align::Start);
|
|
||||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
label.set_attributes(Some(&attributes));
|
|
||||||
let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
||||||
wrap.set_border_width(6);
|
|
||||||
wrap.add(&label);
|
|
||||||
|
|
||||||
let row = gtk::ListBoxRow::new();
|
|
||||||
row.set_activatable(false);
|
|
||||||
row.add(&wrap);
|
|
||||||
row.show_all();
|
|
||||||
|
|
||||||
list.insert(
|
|
||||||
&row,
|
|
||||||
(section.before_index + section_count).try_into().unwrap(),
|
|
||||||
);
|
|
||||||
section_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the closure to be called when the track was edited.
|
|
||||||
pub fn set_ready_cb<F: Fn(Track) -> () + 'static>(&self, cb: F) {
|
|
||||||
self.ready_cb.replace(Some(Box::new(cb)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NavigatorScreen for TrackEditor {
|
|
||||||
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
|
||||||
self.navigator.replace(Some(navigator));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_widget(&self) -> gtk::Widget {
|
|
||||||
self.widget.clone().upcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detach_navigator(&self) {
|
|
||||||
self.navigator.replace(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
580
musicus/src/editors/track_set.rs
Normal file
580
musicus/src/editors/track_set.rs
Normal file
|
|
@ -0,0 +1,580 @@
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::database::{Recording, Track, TrackSet};
|
||||||
|
use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector};
|
||||||
|
use crate::widgets::{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::rc::Rc;
|
||||||
|
|
||||||
|
/// Representation of a track that can be imported into the music library.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct TrackSource {
|
||||||
|
/// A short string identifying the track for the user.
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
/// Whether the track is ready to be imported.
|
||||||
|
pub ready: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Representation of a medium that can be imported into the music library.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct MediumSource {
|
||||||
|
/// The tracks that can be imported from the medium.
|
||||||
|
pub tracks: Vec<TrackSource>,
|
||||||
|
|
||||||
|
/// Whether all tracks are ready to be imported.
|
||||||
|
pub ready: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediumSource {
|
||||||
|
/// Create a dummy medium source for testing purposes.
|
||||||
|
fn dummy() -> Self {
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
|
||||||
|
for index in 0..20 {
|
||||||
|
tracks.push(TrackSource {
|
||||||
|
description: format!("Track {}", index + 1),
|
||||||
|
ready: Cell::new(true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tracks,
|
||||||
|
ready: Cell::new(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A track while being edited.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct TrackData<'a> {
|
||||||
|
/// A reference to the selected track source.
|
||||||
|
pub source: &'a TrackSource,
|
||||||
|
|
||||||
|
/// The actual value for the track.
|
||||||
|
pub track: Track,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A track set while being edited.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct TrackSetData<'a> {
|
||||||
|
/// The recording to which the tracks belong.
|
||||||
|
pub recording: Option<Recording>,
|
||||||
|
|
||||||
|
/// The tracks that are being edited.
|
||||||
|
pub tracks: Vec<TrackData<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSetData {
|
||||||
|
/// Create a new empty track set.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
recording: None,
|
||||||
|
tracks: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A screen for editing a set of tracks for one recording.
|
||||||
|
pub struct TrackSetEditor {
|
||||||
|
backend: Rc<Backend>,
|
||||||
|
source: Rc<RefCell<MediumSource>>,
|
||||||
|
widget: gtk::Box,
|
||||||
|
save_button: gtk::Button,
|
||||||
|
recording_row: libhandy::ActionRow,
|
||||||
|
track_list: List,
|
||||||
|
data: RefCell<TrackSetData>,
|
||||||
|
done_cb: RefCell<Option<Box<dyn Fn(TrackSet)>>>,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSetEditor {
|
||||||
|
/// Create a new track set editor.
|
||||||
|
pub fn new(backend: Rc<Backend>, source: Rc<TrackSource>) -> Rc<Self> {
|
||||||
|
// TODO: Replace with argument.
|
||||||
|
let source = Rc::new(RefCell::new(MediumSource::dummy()));
|
||||||
|
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, save_button);
|
||||||
|
get_widget!(builder, libhandy::ActionRow, recording_row);
|
||||||
|
get_widget!(builder, gtk::Button, select_recording_button);
|
||||||
|
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 this = Rc::new(Self {
|
||||||
|
backend,
|
||||||
|
source,
|
||||||
|
widget,
|
||||||
|
save_button,
|
||||||
|
recording_row,
|
||||||
|
track_list,
|
||||||
|
data: RefCell::new(TrackSetData::new()),
|
||||||
|
done_cb: RefCell::new(None),
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.save_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
if let Some(cb) = &*this.done_cb.borrow() {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
select_recording_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let person_selector = PersonSelector::new(this.backend.clone());
|
||||||
|
|
||||||
|
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
|
||||||
|
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
|
||||||
|
|
||||||
|
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
|
||||||
|
let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone());
|
||||||
|
|
||||||
|
recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| {
|
||||||
|
let mut data = this.data.borrow_mut();
|
||||||
|
data.recording = Some(recording);
|
||||||
|
this.recording_selected();
|
||||||
|
|
||||||
|
navigator.clone().pop();
|
||||||
|
navigator.clone().pop();
|
||||||
|
navigator.clone().pop();
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.clone().push(recording_selector);
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.clone().push(work_selector);
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.clone().push(person_selector);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
edit_tracks_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
let selector = TrackSelector::new(Rc::clone(this.source));
|
||||||
|
|
||||||
|
selector.set_selected_cb(clone!(@strong this => move |selection| {
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
|
||||||
|
for index in selection {
|
||||||
|
let track = Track {
|
||||||
|
work_parts: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = this.source.tracks[index].clone();
|
||||||
|
|
||||||
|
let data = TrackData {
|
||||||
|
track,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracks.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = tracks.len();
|
||||||
|
this.tracks.replace(tracks);
|
||||||
|
this.track_list.update(length);
|
||||||
|
this.autofill_parts();
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.push(selector);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.track_list.set_make_widget(clone!(@strong this => move |index| {
|
||||||
|
let data = &this.tracks.borrow()[index];
|
||||||
|
|
||||||
|
let mut title_parts = Vec::<String>::new();
|
||||||
|
|
||||||
|
if let Some(recording) = &*this.recording.borrow() {
|
||||||
|
for part in &data.track.work_parts {
|
||||||
|
title_parts.push(recording.work.parts[*part].title.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = if title_parts.is_empty() {
|
||||||
|
gettext("Unknown")
|
||||||
|
} else {
|
||||||
|
title_parts.join(", ")
|
||||||
|
};
|
||||||
|
|
||||||
|
let subtitle = data.source.description.clone();
|
||||||
|
|
||||||
|
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
|
||||||
|
let edit_button = gtk::Button::new();
|
||||||
|
edit_button.set_relief(gtk::ReliefStyle::None);
|
||||||
|
edit_button.set_valign(gtk::Align::Center);
|
||||||
|
edit_button.add(&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.set_activatable_widget(Some(&edit_button));
|
||||||
|
row.show_all();
|
||||||
|
|
||||||
|
edit_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let recording = this.recording.borrow().clone();
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
|
||||||
|
if let (Some(recording), Some(navigator)) = (recording, navigator) {
|
||||||
|
let editor = TrackEditor::new(recording, Vec::new());
|
||||||
|
|
||||||
|
editor.set_selected_cb(clone!(@strong this => move |selection| {
|
||||||
|
{
|
||||||
|
let mut tracks = &mut this.data.borrow_mut().tracks;
|
||||||
|
let mut track = &mut tracks[index];
|
||||||
|
track.track.work_parts = selection;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update_tracks();
|
||||||
|
}));
|
||||||
|
|
||||||
|
navigator.push(editor);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
row.upcast()
|
||||||
|
}));
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure to be called when the user has created the track set.
|
||||||
|
pub fn set_done_cb<F: Fn(TrackSet) + 'static>(&self, cb: F) {
|
||||||
|
self.done_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set everything up after selecting a recording.
|
||||||
|
fn recording_selected(&self) {
|
||||||
|
if let Some(recording) = self.data.borrow().recording {
|
||||||
|
self.recording_row.set_title(Some(&recording.work.get_title()));
|
||||||
|
self.recording_row.set_subtitle(Some(&recording.get_performers()));
|
||||||
|
self.save_button.set_sensitive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.autofill_parts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Automatically try to put work part information from the selected recording into the
|
||||||
|
/// selected tracks.
|
||||||
|
fn autofill_parts(&self) {
|
||||||
|
if let Some(recording) = self.data.borrow().recording {
|
||||||
|
let mut tracks = self.tracks.borrow_mut();
|
||||||
|
|
||||||
|
for (index, _) in recording.work.parts.iter().enumerate() {
|
||||||
|
if let Some(mut data) = tracks.get_mut(index) {
|
||||||
|
data.track.work_parts = vec![index];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_tracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the track list.
|
||||||
|
fn update_tracks(&self) {
|
||||||
|
let length = self.data.borrow().tracks.len();
|
||||||
|
self.track_list.update(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for TrackSetEditor {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A screen for selecting tracks from a medium.
|
||||||
|
struct TrackSelector {
|
||||||
|
source: Rc<RefCell<MediumSource>>,
|
||||||
|
widget: gtk::Box,
|
||||||
|
select_button: gtk::Button,
|
||||||
|
selection: RefCell<Vec<usize>>,
|
||||||
|
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSelector {
|
||||||
|
/// Create a new track selector.
|
||||||
|
pub fn new(source: Rc<RefCell<MediumSource>>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, select_button);
|
||||||
|
get_widget!(builder, gtk::Frame, tracks_frame);
|
||||||
|
|
||||||
|
let track_list = gtk::ListBox::new();
|
||||||
|
track_list.set_selection_mode(gtk::SelectionMode::None);
|
||||||
|
track_list.set_vexpand(false);
|
||||||
|
track_list.show();
|
||||||
|
tracks_frame.add(&track_list);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
source,
|
||||||
|
widget,
|
||||||
|
select_button,
|
||||||
|
selection: RefCell::new(Vec::new()),
|
||||||
|
selected_cb: RefCell::new(None),
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.select_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||||
|
let selection = this.selection.borrow().clone();
|
||||||
|
cb(selection);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (index, track) in this.tracks.iter().enumerate() {
|
||||||
|
let check = gtk::CheckButton::new();
|
||||||
|
|
||||||
|
check.connect_toggled(clone!(@strong this => move |check| {
|
||||||
|
let mut selection = this.selection.borrow_mut();
|
||||||
|
if check.get_active() {
|
||||||
|
selection.push(index);
|
||||||
|
} else {
|
||||||
|
if let Some(pos) = selection.iter().position(|part| *part == index) {
|
||||||
|
selection.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selection.is_empty() {
|
||||||
|
this.select_button.set_sensitive(false);
|
||||||
|
} else {
|
||||||
|
this.select_button.set_sensitive(true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let row = libhandy::ActionRow::new();
|
||||||
|
row.add_prefix(&check);
|
||||||
|
row.set_activatable_widget(Some(&check));
|
||||||
|
row.set_title(Some(&track.description));
|
||||||
|
row.show_all();
|
||||||
|
|
||||||
|
track_list.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure to be called when the user has selected tracks. The
|
||||||
|
/// closure will be called with the indices of the selected tracks as its
|
||||||
|
/// argument.
|
||||||
|
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
|
||||||
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for TrackSelector {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A screen for editing a single track.
|
||||||
|
struct TrackEditor {
|
||||||
|
widget: gtk::Box,
|
||||||
|
selection: RefCell<Vec<usize>>,
|
||||||
|
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
|
||||||
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackEditor {
|
||||||
|
/// Create a new track editor.
|
||||||
|
pub fn new(recording: Recording, selection: Vec<usize>) -> Rc<Self> {
|
||||||
|
// Create UI
|
||||||
|
|
||||||
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
|
||||||
|
|
||||||
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
get_widget!(builder, gtk::Button, back_button);
|
||||||
|
get_widget!(builder, gtk::Button, select_button);
|
||||||
|
get_widget!(builder, gtk::Frame, parts_frame);
|
||||||
|
|
||||||
|
let parts_list = gtk::ListBox::new();
|
||||||
|
parts_list.set_selection_mode(gtk::SelectionMode::None);
|
||||||
|
parts_list.set_vexpand(false);
|
||||||
|
parts_list.show();
|
||||||
|
parts_frame.add(&parts_list);
|
||||||
|
|
||||||
|
let this = Rc::new(Self {
|
||||||
|
widget,
|
||||||
|
selection: RefCell::new(selection),
|
||||||
|
selected_cb: RefCell::new(None),
|
||||||
|
navigator: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect signals and callbacks
|
||||||
|
|
||||||
|
back_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
select_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
|
let navigator = this.navigator.borrow().clone();
|
||||||
|
if let Some(navigator) = navigator {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||||
|
let selection = this.selection.borrow().clone();
|
||||||
|
cb(selection);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (index, part) in recording.work.parts.iter().enumerate() {
|
||||||
|
let check = gtk::CheckButton::new();
|
||||||
|
|
||||||
|
check.connect_toggled(clone!(@strong this => move |check| {
|
||||||
|
let mut selection = this.selection.borrow_mut();
|
||||||
|
if check.get_active() {
|
||||||
|
selection.push(index);
|
||||||
|
} else {
|
||||||
|
if let Some(pos) = selection.iter().position(|part| *part == index) {
|
||||||
|
selection.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let row = libhandy::ActionRow::new();
|
||||||
|
row.add_prefix(&check);
|
||||||
|
row.set_activatable_widget(Some(&check));
|
||||||
|
row.set_title(Some(&part.title));
|
||||||
|
row.show_all();
|
||||||
|
|
||||||
|
parts_list.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure to be called when the user has edited the track.
|
||||||
|
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
|
||||||
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigatorScreen for TrackEditor {
|
||||||
|
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
||||||
|
self.navigator.replace(Some(navigator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_navigator(&self) {
|
||||||
|
self.navigator.replace(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple list of widgets.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
musicus/src/editors/track_source.rs
Normal file
42
musicus/src/editors/track_source.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// One track within a [`TrackSource`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TrackState {
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A live representation of a source of audio tracks.
|
||||||
|
pub struct TrackSource {
|
||||||
|
pub tracks: Vec<TrackState>,
|
||||||
|
pub ready: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSource {
|
||||||
|
/// Create a new track source for a folder. This will provide the folder's
|
||||||
|
/// files as selectable tracks and be ready immediately.
|
||||||
|
pub fn folder(path: &Path) -> Result<Self> {
|
||||||
|
let mut tracks = Vec::<TrackState>::new();
|
||||||
|
|
||||||
|
let entries = std::fs::read_dir(path)?;
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type()?.is_file() {
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
let track = TrackState { description: file_name.to_str().unwrap().to_owned() };
|
||||||
|
tracks.push(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks.sort_unstable_by(|a, b| {
|
||||||
|
a.description.cmp(&b.description)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tracks,
|
||||||
|
ready: Cell::new(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,337 +0,0 @@
|
||||||
use super::track::TrackEditor;
|
|
||||||
use crate::backend::Backend;
|
|
||||||
use crate::database::*;
|
|
||||||
use crate::widgets::{List, Navigator, NavigatorScreen};
|
|
||||||
use crate::selectors::{PersonSelector, WorkSelector, RecordingSelector};
|
|
||||||
use gettextrs::gettext;
|
|
||||||
use glib::clone;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk_macros::get_widget;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// A dialog for editing a set of tracks.
|
|
||||||
// TODO: Disable buttons if no track is selected.
|
|
||||||
pub struct TracksEditor {
|
|
||||||
backend: Rc<Backend>,
|
|
||||||
widget: gtk::Box,
|
|
||||||
save_button: gtk::Button,
|
|
||||||
recording_stack: gtk::Stack,
|
|
||||||
work_label: gtk::Label,
|
|
||||||
performers_label: gtk::Label,
|
|
||||||
track_list: Rc<List<Track>>,
|
|
||||||
recording: RefCell<Option<Recording>>,
|
|
||||||
tracks: RefCell<Vec<Track>>,
|
|
||||||
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TracksEditor {
|
|
||||||
/// Create a new track editor an optionally initialize it with a recording and a list of
|
|
||||||
/// tracks.
|
|
||||||
pub fn new(
|
|
||||||
backend: Rc<Backend>,
|
|
||||||
recording: Option<Recording>,
|
|
||||||
tracks: Vec<Track>,
|
|
||||||
) -> Rc<Self> {
|
|
||||||
// UI setup
|
|
||||||
|
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui");
|
|
||||||
|
|
||||||
get_widget!(builder, gtk::Box, widget);
|
|
||||||
get_widget!(builder, gtk::Button, back_button);
|
|
||||||
get_widget!(builder, gtk::Button, save_button);
|
|
||||||
get_widget!(builder, gtk::Button, recording_button);
|
|
||||||
get_widget!(builder, gtk::Stack, recording_stack);
|
|
||||||
get_widget!(builder, gtk::Label, work_label);
|
|
||||||
get_widget!(builder, gtk::Label, performers_label);
|
|
||||||
get_widget!(builder, gtk::ScrolledWindow, scroll);
|
|
||||||
get_widget!(builder, gtk::Button, add_track_button);
|
|
||||||
get_widget!(builder, gtk::Button, edit_track_button);
|
|
||||||
get_widget!(builder, gtk::Button, remove_track_button);
|
|
||||||
get_widget!(builder, gtk::Button, move_track_up_button);
|
|
||||||
get_widget!(builder, gtk::Button, move_track_down_button);
|
|
||||||
|
|
||||||
let track_list = List::new(&gettext("Add some tracks."));
|
|
||||||
scroll.add(&track_list.widget);
|
|
||||||
|
|
||||||
let this = Rc::new(Self {
|
|
||||||
backend,
|
|
||||||
widget,
|
|
||||||
save_button,
|
|
||||||
recording_stack,
|
|
||||||
work_label,
|
|
||||||
performers_label,
|
|
||||||
track_list,
|
|
||||||
recording: RefCell::new(recording),
|
|
||||||
tracks: RefCell::new(tracks),
|
|
||||||
callback: RefCell::new(None),
|
|
||||||
navigator: RefCell::new(None),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Signals and callbacks
|
|
||||||
|
|
||||||
back_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.save_button
|
|
||||||
.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let context = glib::MainContext::default();
|
|
||||||
let this = this.clone();
|
|
||||||
context.spawn_local(async move {
|
|
||||||
let recording = this.recording.borrow().as_ref().unwrap().clone();
|
|
||||||
|
|
||||||
// Add the recording first, if it's from the server.
|
|
||||||
|
|
||||||
if !this.backend.db().recording_exists(&recording.id).await.unwrap() {
|
|
||||||
this.backend.db().update_recording(recording.clone()).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the actual tracks.
|
|
||||||
|
|
||||||
this.backend.db().update_tracks(
|
|
||||||
&recording.id,
|
|
||||||
this.tracks.borrow().clone(),
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(callback) = &*this.callback.borrow() {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
recording_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
let person_selector = PersonSelector::new(this.backend.clone());
|
|
||||||
|
|
||||||
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
|
|
||||||
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
|
|
||||||
|
|
||||||
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
|
|
||||||
let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone());
|
|
||||||
|
|
||||||
recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| {
|
|
||||||
this.recording_selected(recording);
|
|
||||||
this.recording.replace(Some(recording.clone()));
|
|
||||||
|
|
||||||
navigator.clone().pop();
|
|
||||||
navigator.clone().pop();
|
|
||||||
navigator.clone().pop();
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.clone().push(recording_selector);
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.clone().push(work_selector);
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.clone().push(person_selector);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.track_list
|
|
||||||
.set_make_widget(clone!(@strong this => move |track| {
|
|
||||||
this.build_track_row(track)
|
|
||||||
}));
|
|
||||||
|
|
||||||
add_track_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
let music_library_path = this.backend.get_music_library_path().unwrap();
|
|
||||||
|
|
||||||
let dialog = gtk::FileChooserNative::new(
|
|
||||||
Some(&gettext("Select audio files")),
|
|
||||||
Some(&navigator.window),
|
|
||||||
gtk::FileChooserAction::Open,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
dialog.set_select_multiple(true);
|
|
||||||
dialog.set_current_folder(&music_library_path);
|
|
||||||
|
|
||||||
if let gtk::ResponseType::Accept = dialog.run() {
|
|
||||||
let mut index = match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => index + 1,
|
|
||||||
None => this.tracks.borrow().len(),
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
for file_name in dialog.get_filenames() {
|
|
||||||
let file_name = file_name.strip_prefix(&music_library_path).unwrap();
|
|
||||||
tracks.insert(index, Track {
|
|
||||||
work_parts: Vec::new(),
|
|
||||||
file_name: String::from(file_name.to_str().unwrap()),
|
|
||||||
});
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.track_list.show_items(this.tracks.borrow().clone());
|
|
||||||
this.autofill_parts();
|
|
||||||
this.track_list.select_index(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
remove_track_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
tracks.remove(index);
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index);
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
move_track_up_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => {
|
|
||||||
if index > 0 {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
tracks.swap(index - 1, index);
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
move_track_down_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
match this.track_list.get_selected_index() {
|
|
||||||
Some(index) => {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
if index < tracks.len() - 1 {
|
|
||||||
tracks.swap(index, index + 1);
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
edit_track_button.connect_clicked(clone!(@strong this => move |_| {
|
|
||||||
let navigator = this.navigator.borrow().clone();
|
|
||||||
if let Some(navigator) = navigator {
|
|
||||||
if let Some(index) = this.track_list.get_selected_index() {
|
|
||||||
if let Some(recording) = &*this.recording.borrow() {
|
|
||||||
let editor = TrackEditor::new(this.tracks.borrow()[index].clone(), recording.work.clone());
|
|
||||||
|
|
||||||
editor.set_ready_cb(clone!(@strong this => move |track| {
|
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
|
||||||
tracks[index] = track;
|
|
||||||
this.track_list.show_items(tracks.clone());
|
|
||||||
this.track_list.select_index(index);
|
|
||||||
}));
|
|
||||||
|
|
||||||
navigator.push(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Initialization
|
|
||||||
|
|
||||||
if let Some(recording) = &*this.recording.borrow() {
|
|
||||||
this.recording_selected(recording);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.track_list.show_items(this.tracks.borrow().clone());
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a callback to be called when the tracks are saved.
|
|
||||||
pub fn set_callback<F: Fn() -> () + 'static>(&self, cb: F) {
|
|
||||||
self.callback.replace(Some(Box::new(cb)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a widget representing a track.
|
|
||||||
fn build_track_row(&self, track: &Track) -> gtk::Widget {
|
|
||||||
let mut title_parts = Vec::<String>::new();
|
|
||||||
for part in &track.work_parts {
|
|
||||||
if let Some(recording) = &*self.recording.borrow() {
|
|
||||||
title_parts.push(recording.work.parts[*part].title.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = if title_parts.is_empty() {
|
|
||||||
gettext("Unknown")
|
|
||||||
} else {
|
|
||||||
title_parts.join(", ")
|
|
||||||
};
|
|
||||||
|
|
||||||
let title_label = gtk::Label::new(Some(&title));
|
|
||||||
title_label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
title_label.set_halign(gtk::Align::Start);
|
|
||||||
|
|
||||||
let file_name_label = gtk::Label::new(Some(&track.file_name));
|
|
||||||
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
|
|
||||||
file_name_label.set_opacity(0.5);
|
|
||||||
file_name_label.set_halign(gtk::Align::Start);
|
|
||||||
|
|
||||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
||||||
vbox.set_border_width(6);
|
|
||||||
vbox.add(&title_label);
|
|
||||||
vbox.add(&file_name_label);
|
|
||||||
|
|
||||||
vbox.upcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set everything up after selecting a recording.
|
|
||||||
fn recording_selected(&self, recording: &Recording) {
|
|
||||||
self.work_label.set_text(&recording.work.get_title());
|
|
||||||
self.performers_label.set_text(&recording.get_performers());
|
|
||||||
self.recording_stack.set_visible_child_name("selected");
|
|
||||||
self.save_button.set_sensitive(true);
|
|
||||||
self.autofill_parts();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Automatically try to put work part information from the selected recording into the
|
|
||||||
/// selected tracks.
|
|
||||||
fn autofill_parts(&self) {
|
|
||||||
if let Some(recording) = &*self.recording.borrow() {
|
|
||||||
let mut tracks = self.tracks.borrow_mut();
|
|
||||||
|
|
||||||
for (index, _) in recording.work.parts.iter().enumerate() {
|
|
||||||
if let Some(mut track) = tracks.get_mut(index) {
|
|
||||||
track.work_parts = vec![index];
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.track_list.show_items(tracks.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NavigatorScreen for TracksEditor {
|
|
||||||
fn attach_navigator(&self, navigator: Rc<Navigator>) {
|
|
||||||
self.navigator.replace(Some(navigator));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_widget(&self) -> gtk::Widget {
|
|
||||||
self.widget.clone().upcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detach_navigator(&self) {
|
|
||||||
self.navigator.replace(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -33,9 +33,9 @@ run_command(
|
||||||
)
|
)
|
||||||
|
|
||||||
sources = files(
|
sources = files(
|
||||||
'backend/client/mod.rs',
|
|
||||||
'backend/client/ensembles.rs',
|
'backend/client/ensembles.rs',
|
||||||
'backend/client/instruments.rs',
|
'backend/client/instruments.rs',
|
||||||
|
'backend/client/mod.rs',
|
||||||
'backend/client/persons.rs',
|
'backend/client/persons.rs',
|
||||||
'backend/client/recordings.rs',
|
'backend/client/recordings.rs',
|
||||||
'backend/client/works.rs',
|
'backend/client/works.rs',
|
||||||
|
|
@ -43,16 +43,18 @@ sources = files(
|
||||||
'backend/mod.rs',
|
'backend/mod.rs',
|
||||||
'backend/secure.rs',
|
'backend/secure.rs',
|
||||||
'database/ensembles.rs',
|
'database/ensembles.rs',
|
||||||
|
'database/files.rs',
|
||||||
'database/instruments.rs',
|
'database/instruments.rs',
|
||||||
|
'database/medium.rs',
|
||||||
'database/mod.rs',
|
'database/mod.rs',
|
||||||
'database/persons.rs',
|
'database/persons.rs',
|
||||||
'database/recordings.rs',
|
'database/recordings.rs',
|
||||||
'database/schema.rs',
|
'database/schema.rs',
|
||||||
'database/thread.rs',
|
'database/thread.rs',
|
||||||
'database/tracks.rs',
|
|
||||||
'database/works.rs',
|
'database/works.rs',
|
||||||
'dialogs/about.rs',
|
'dialogs/about.rs',
|
||||||
'dialogs/import_disc.rs',
|
'dialogs/import_disc.rs',
|
||||||
|
'dialogs/import_folder.rs',
|
||||||
'dialogs/login_dialog.rs',
|
'dialogs/login_dialog.rs',
|
||||||
'dialogs/mod.rs',
|
'dialogs/mod.rs',
|
||||||
'dialogs/preferences.rs',
|
'dialogs/preferences.rs',
|
||||||
|
|
@ -63,8 +65,7 @@ sources = files(
|
||||||
'editors/performance.rs',
|
'editors/performance.rs',
|
||||||
'editors/person.rs',
|
'editors/person.rs',
|
||||||
'editors/recording.rs',
|
'editors/recording.rs',
|
||||||
'editors/track.rs',
|
'editors/track_set.rs',
|
||||||
'editors/tracks.rs',
|
|
||||||
'editors/work.rs',
|
'editors/work.rs',
|
||||||
'editors/work_part.rs',
|
'editors/work_part.rs',
|
||||||
'editors/work_section.rs',
|
'editors/work_section.rs',
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use std::rc::Rc;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlaylistItem {
|
pub struct PlaylistItem {
|
||||||
pub tracks: TrackSet,
|
pub tracks: TrackSet,
|
||||||
|
pub file_names: Vec<String>,
|
||||||
pub indices: Vec<usize>,
|
pub indices: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,15 +249,7 @@ impl Player {
|
||||||
"file://{}",
|
"file://{}",
|
||||||
self.music_library_path
|
self.music_library_path
|
||||||
.join(
|
.join(
|
||||||
self.playlist
|
self.playlist.borrow()[current_item].file_names[current_track].clone(),
|
||||||
.borrow()
|
|
||||||
.get(current_item)
|
|
||||||
.ok_or(anyhow!("Playlist item doesn't exist!"))?
|
|
||||||
.tracks
|
|
||||||
.get(current_track)
|
|
||||||
.ok_or(anyhow!("Track doesn't exist!"))?
|
|
||||||
.file_name
|
|
||||||
.clone(),
|
|
||||||
)
|
)
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
|
||||||
|
|
@ -104,13 +104,6 @@ impl Ripper {
|
||||||
/// Build the GStreamer pipeline to rip a track.
|
/// Build the GStreamer pipeline to rip a track.
|
||||||
fn build_pipeline(path: &str, track: u32) -> Result<Pipeline> {
|
fn build_pipeline(path: &str, track: u32) -> Result<Pipeline> {
|
||||||
let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?;
|
let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?;
|
||||||
|
|
||||||
// // TODO: Remove.
|
|
||||||
// cdparanoiasrc.set_property(
|
|
||||||
// "device",
|
|
||||||
// &String::from("/home/johrpan/Diverses/arrau_schumann.iso"),
|
|
||||||
// )?;
|
|
||||||
|
|
||||||
cdparanoiasrc.set_property("track", &track)?;
|
cdparanoiasrc.set_property("track", &track)?;
|
||||||
|
|
||||||
let queue = ElementFactory::make("queue", None)?;
|
let queue = ElementFactory::make("queue", None)?;
|
||||||
|
|
|
||||||
|
|
@ -215,15 +215,17 @@ impl PlayerScreen {
|
||||||
elements.push(PlaylistElement {
|
elements.push(PlaylistElement {
|
||||||
item: item_index,
|
item: item_index,
|
||||||
track: 0,
|
track: 0,
|
||||||
title: item.recording.work.get_title(),
|
title: item.tracks.recording.work.get_title(),
|
||||||
subtitle: Some(item.recording.get_performers()),
|
subtitle: Some(item.tracks.recording.get_performers()),
|
||||||
playable: false,
|
playable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (track_index, track) in item.tracks.iter().enumerate() {
|
for track_index in &item.indices {
|
||||||
|
let track = &item.tracks.tracks[*track_index];
|
||||||
|
|
||||||
let mut parts = Vec::<String>::new();
|
let mut parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
parts.push(item.recording.work.parts[*part].title.clone());
|
parts.push(item.tracks.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = if parts.is_empty() {
|
let title = if parts.is_empty() {
|
||||||
|
|
@ -234,7 +236,7 @@ impl PlayerScreen {
|
||||||
|
|
||||||
elements.push(PlaylistElement {
|
elements.push(PlaylistElement {
|
||||||
item: item_index,
|
item: item_index,
|
||||||
track: track_index,
|
track: *track_index,
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: None,
|
subtitle: None,
|
||||||
playable: true,
|
playable: true,
|
||||||
|
|
@ -262,20 +264,20 @@ impl PlayerScreen {
|
||||||
next_button.set_sensitive(player.has_next());
|
next_button.set_sensitive(player.has_next());
|
||||||
|
|
||||||
let item = &playlist.borrow()[current_item];
|
let item = &playlist.borrow()[current_item];
|
||||||
let track = &item.tracks[current_track];
|
let track = &item.tracks.tracks[current_track];
|
||||||
|
|
||||||
let mut parts = Vec::<String>::new();
|
let mut parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
parts.push(item.recording.work.parts[*part].title.clone());
|
parts.push(item.tracks.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = item.recording.work.get_title();
|
let mut title = item.tracks.recording.work.get_title();
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
title = format!("{}: {}", title, parts.join(", "));
|
title = format!("{}: {}", title, parts.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
title_label.set_text(&title);
|
title_label.set_text(&title);
|
||||||
subtitle_label.set_text(&item.recording.get_performers());
|
subtitle_label.set_text(&item.tracks.recording.get_performers());
|
||||||
position_label.set_text("0:00");
|
position_label.set_text("0:00");
|
||||||
|
|
||||||
self_item.replace(current_item);
|
self_item.replace(current_item);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::backend::*;
|
use crate::backend::*;
|
||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::editors::{RecordingEditor, TracksEditor};
|
use crate::editors::RecordingEditor;
|
||||||
use crate::player::*;
|
use crate::player::*;
|
||||||
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
|
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
|
||||||
use gettextrs::gettext;
|
use gettextrs::gettext;
|
||||||
|
|
@ -76,15 +76,15 @@ impl RecordingScreen {
|
||||||
title_label.set_ellipsize(pango::EllipsizeMode::End);
|
title_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
title_label.set_halign(gtk::Align::Start);
|
title_label.set_halign(gtk::Align::Start);
|
||||||
|
|
||||||
let file_name_label = gtk::Label::new(Some(&track.file_name));
|
// let file_name_label = gtk::Label::new(Some(&track.file_name));
|
||||||
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
|
// file_name_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
file_name_label.set_opacity(0.5);
|
// file_name_label.set_opacity(0.5);
|
||||||
file_name_label.set_halign(gtk::Align::Start);
|
// file_name_label.set_halign(gtk::Align::Start);
|
||||||
|
|
||||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
vbox.set_border_width(6);
|
vbox.set_border_width(6);
|
||||||
vbox.add(&title_label);
|
vbox.add(&title_label);
|
||||||
vbox.add(&file_name_label);
|
// vbox.add(&file_name_label);
|
||||||
|
|
||||||
vbox.upcast()
|
vbox.upcast()
|
||||||
}));
|
}));
|
||||||
|
|
@ -98,10 +98,10 @@ impl RecordingScreen {
|
||||||
|
|
||||||
add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| {
|
add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| {
|
||||||
if let Some(player) = result.backend.get_player() {
|
if let Some(player) = result.backend.get_player() {
|
||||||
player.add_item(PlaylistItem {
|
// player.add_item(PlaylistItem {
|
||||||
recording: result.recording.clone(),
|
// recording: result.recording.clone(),
|
||||||
tracks: result.tracks.borrow().clone(),
|
// tracks: result.tracks.borrow().clone(),
|
||||||
}).unwrap();
|
// }).unwrap();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -121,33 +121,33 @@ impl RecordingScreen {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||||
let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone());
|
// let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone());
|
||||||
let window = NavigatorWindow::new(editor);
|
// let window = NavigatorWindow::new(editor);
|
||||||
window.show();
|
// window.show();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
|
// clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
|
||||||
clone.backend.library_changed();
|
// clone.backend.library_changed();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let tracks = clone
|
// let tracks = clone
|
||||||
.backend
|
// .backend
|
||||||
.db()
|
// .db()
|
||||||
.get_tracks(&clone.recording.id)
|
// .get_tracks(&clone.recording.id)
|
||||||
.await
|
// .await
|
||||||
.unwrap();
|
// .unwrap();
|
||||||
|
|
||||||
list.show_items(tracks.clone());
|
// list.show_items(tracks.clone());
|
||||||
clone.stack.set_visible_child_name("content");
|
// clone.stack.set_visible_child_name("content");
|
||||||
clone.tracks.replace(tracks);
|
// clone.tracks.replace(tracks);
|
||||||
});
|
});
|
||||||
|
|
||||||
result
|
result
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,16 @@ where
|
||||||
this
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
|
||||||
self.make_widget.replace(Some(Box::new(make_widget)));
|
self.make_widget.replace(Some(Box::new(make_widget)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,20 +112,20 @@ impl PlayerBar {
|
||||||
next_button.set_sensitive(player.has_next());
|
next_button.set_sensitive(player.has_next());
|
||||||
|
|
||||||
let item = &playlist.borrow()[current_item];
|
let item = &playlist.borrow()[current_item];
|
||||||
let track = &item.tracks[current_track];
|
let track = &item.tracks.tracks[current_track];
|
||||||
|
|
||||||
let mut parts = Vec::<String>::new();
|
let mut parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
parts.push(item.recording.work.parts[*part].title.clone());
|
parts.push(item.tracks.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = item.recording.work.get_title();
|
let mut title = item.tracks.recording.work.get_title();
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
title = format!("{}: {}", title, parts.join(", "));
|
title = format!("{}: {}", title, parts.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
title_label.set_text(&title);
|
title_label.set_text(&title);
|
||||||
subtitle_label.set_text(&item.recording.get_performers());
|
subtitle_label.set_text(&item.tracks.recording.get_performers());
|
||||||
position_label.set_text("0:00");
|
position_label.set_text("0:00");
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::backend::*;
|
use crate::backend::*;
|
||||||
use crate::dialogs::*;
|
use crate::dialogs::*;
|
||||||
use crate::editors::TracksEditor;
|
|
||||||
use crate::screens::*;
|
use crate::screens::*;
|
||||||
use crate::widgets::*;
|
use crate::widgets::*;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
|
|
@ -85,13 +84,17 @@ impl Window {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
add_button.connect_clicked(clone!(@strong result => move |_| {
|
add_button.connect_clicked(clone!(@strong result => move |_| {
|
||||||
let editor = TracksEditor::new(result.backend.clone(), None, Vec::new());
|
// let editor = TracksEditor::new(result.backend.clone(), None, Vec::new());
|
||||||
|
|
||||||
editor.set_callback(clone!(@strong result => move || {
|
// editor.set_callback(clone!(@strong result => move || {
|
||||||
result.reload();
|
// result.reload();
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
let window = NavigatorWindow::new(editor);
|
// let window = NavigatorWindow::new(editor);
|
||||||
|
// window.show();
|
||||||
|
|
||||||
|
let dialog = ImportFolderDialog::new(result.backend.clone());
|
||||||
|
let window = NavigatorWindow::new(dialog);
|
||||||
window.show();
|
window.show();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue