Replace old track editors with import dialogs

This commit is contained in:
Elias Projahn 2021-01-13 16:15:13 +01:00
parent c7928003e4
commit 4aa858602d
16 changed files with 856 additions and 552 deletions

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

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

View file

@ -1,3 +1,9 @@
pub mod import;
pub use import::*;
pub mod import_folder;
pub use import_folder::*;
pub mod import_disc;
pub use import_disc::*;

View file

@ -10,13 +10,15 @@ pub use person::*;
pub mod recording;
pub use recording::*;
pub mod tracks;
pub use tracks::*;
pub mod track_set;
pub use track_set::*;
pub mod track_source;
pub use track_source::*;
pub mod work;
pub use work::*;
mod performance;
mod track;
mod work_part;
mod work_section;

View file

@ -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(&section.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);
}
}

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

View 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),
})
}
}

View file

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

View file

@ -33,9 +33,9 @@ run_command(
)
sources = files(
'backend/client/mod.rs',
'backend/client/ensembles.rs',
'backend/client/instruments.rs',
'backend/client/mod.rs',
'backend/client/persons.rs',
'backend/client/recordings.rs',
'backend/client/works.rs',
@ -43,16 +43,18 @@ sources = files(
'backend/mod.rs',
'backend/secure.rs',
'database/ensembles.rs',
'database/files.rs',
'database/instruments.rs',
'database/medium.rs',
'database/mod.rs',
'database/persons.rs',
'database/recordings.rs',
'database/schema.rs',
'database/thread.rs',
'database/tracks.rs',
'database/works.rs',
'dialogs/about.rs',
'dialogs/import_disc.rs',
'dialogs/import_folder.rs',
'dialogs/login_dialog.rs',
'dialogs/mod.rs',
'dialogs/preferences.rs',
@ -63,8 +65,7 @@ sources = files(
'editors/performance.rs',
'editors/person.rs',
'editors/recording.rs',
'editors/track.rs',
'editors/tracks.rs',
'editors/track_set.rs',
'editors/work.rs',
'editors/work_part.rs',
'editors/work_section.rs',

View file

@ -9,6 +9,7 @@ use std::rc::Rc;
#[derive(Clone)]
pub struct PlaylistItem {
pub tracks: TrackSet,
pub file_names: Vec<String>,
pub indices: Vec<usize>,
}
@ -248,15 +249,7 @@ impl Player {
"file://{}",
self.music_library_path
.join(
self.playlist
.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(),
self.playlist.borrow()[current_item].file_names[current_track].clone(),
)
.to_str()
.unwrap(),

View file

@ -104,13 +104,6 @@ impl Ripper {
/// Build the GStreamer pipeline to rip a track.
fn build_pipeline(path: &str, track: u32) -> Result<Pipeline> {
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)?;
let queue = ElementFactory::make("queue", None)?;

View file

@ -215,15 +215,17 @@ impl PlayerScreen {
elements.push(PlaylistElement {
item: item_index,
track: 0,
title: item.recording.work.get_title(),
subtitle: Some(item.recording.get_performers()),
title: item.tracks.recording.work.get_title(),
subtitle: Some(item.tracks.recording.get_performers()),
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();
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() {
@ -234,7 +236,7 @@ impl PlayerScreen {
elements.push(PlaylistElement {
item: item_index,
track: track_index,
track: *track_index,
title: title,
subtitle: None,
playable: true,
@ -262,20 +264,20 @@ impl PlayerScreen {
next_button.set_sensitive(player.has_next());
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();
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() {
title = format!("{}: {}", title, parts.join(", "));
}
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");
self_item.replace(current_item);

View file

@ -1,6 +1,6 @@
use crate::backend::*;
use crate::database::*;
use crate::editors::{RecordingEditor, TracksEditor};
use crate::editors::RecordingEditor;
use crate::player::*;
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
use gettextrs::gettext;
@ -76,15 +76,15 @@ impl RecordingScreen {
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 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.add(&file_name_label);
vbox.upcast()
}));
@ -98,10 +98,10 @@ impl RecordingScreen {
add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| {
if let Some(player) = result.backend.get_player() {
player.add_item(PlaylistItem {
recording: result.recording.clone(),
tracks: result.tracks.borrow().clone(),
}).unwrap();
// player.add_item(PlaylistItem {
// recording: result.recording.clone(),
// tracks: result.tracks.borrow().clone(),
// }).unwrap();
}
}));
@ -121,33 +121,33 @@ impl RecordingScreen {
}));
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 window = NavigatorWindow::new(editor);
window.show();
// let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone());
// let window = NavigatorWindow::new(editor);
// window.show();
}));
delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
clone.backend.library_changed();
// clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
// clone.backend.library_changed();
});
}));
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let tracks = clone
.backend
.db()
.get_tracks(&clone.recording.id)
.await
.unwrap();
// let tracks = clone
// .backend
// .db()
// .get_tracks(&clone.recording.id)
// .await
// .unwrap();
list.show_items(tracks.clone());
clone.stack.set_visible_child_name("content");
clone.tracks.replace(tracks);
// list.show_items(tracks.clone());
// clone.stack.set_visible_child_name("content");
// clone.tracks.replace(tracks);
});
result

View file

@ -63,6 +63,16 @@ where
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) {
self.make_widget.replace(Some(Box::new(make_widget)));
}

View file

@ -112,20 +112,20 @@ impl PlayerBar {
next_button.set_sensitive(player.has_next());
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();
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() {
title = format!("{}: {}", title, parts.join(", "));
}
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");
}
));

View file

@ -1,6 +1,5 @@
use crate::backend::*;
use crate::dialogs::*;
use crate::editors::TracksEditor;
use crate::screens::*;
use crate::widgets::*;
use futures::prelude::*;
@ -85,13 +84,17 @@ impl Window {
}));
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 || {
result.reload();
}));
// editor.set_callback(clone!(@strong result => move || {
// 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();
}));