mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Merge branch 'medium-tracks' into new-import-screen
This commit is contained in:
commit
3f5f751992
11 changed files with 327 additions and 539 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
use musicus_database::TrackSet;
|
use musicus_database::Track;
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gstreamer_player::prelude::*;
|
use gstreamer_player::prelude::*;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
|
|
@ -10,21 +10,14 @@ use std::sync::Arc;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use mpris_player::{Metadata, MprisPlayer, PlaybackStatus};
|
use mpris_player::{Metadata, MprisPlayer, PlaybackStatus};
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct PlaylistItem {
|
|
||||||
pub track_set: TrackSet,
|
|
||||||
pub indices: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
music_library_path: PathBuf,
|
music_library_path: PathBuf,
|
||||||
player: gstreamer_player::Player,
|
player: gstreamer_player::Player,
|
||||||
playlist: RefCell<Vec<PlaylistItem>>,
|
playlist: RefCell<Vec<Track>>,
|
||||||
current_item: Cell<Option<usize>>,
|
|
||||||
current_track: Cell<Option<usize>>,
|
current_track: Cell<Option<usize>>,
|
||||||
playing: Cell<bool>,
|
playing: Cell<bool>,
|
||||||
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>,
|
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<Track>)>>>,
|
||||||
track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize)>>>,
|
track_cbs: RefCell<Vec<Box<dyn Fn(usize)>>>,
|
||||||
duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
|
duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
|
||||||
playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>,
|
playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>,
|
||||||
position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
|
position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
|
||||||
|
|
@ -47,7 +40,6 @@ impl Player {
|
||||||
music_library_path,
|
music_library_path,
|
||||||
player: player.clone(),
|
player: player.clone(),
|
||||||
playlist: RefCell::new(Vec::new()),
|
playlist: RefCell::new(Vec::new()),
|
||||||
current_item: Cell::new(None),
|
|
||||||
current_track: Cell::new(None),
|
current_track: Cell::new(None),
|
||||||
playing: Cell::new(false),
|
playing: Cell::new(false),
|
||||||
playlist_cbs: RefCell::new(Vec::new()),
|
playlist_cbs: RefCell::new(Vec::new()),
|
||||||
|
|
@ -144,11 +136,11 @@ impl Player {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) + 'static>(&self, cb: F) {
|
pub fn add_playlist_cb<F: Fn(Vec<Track>) + 'static>(&self, cb: F) {
|
||||||
self.playlist_cbs.borrow_mut().push(Box::new(cb));
|
self.playlist_cbs.borrow_mut().push(Box::new(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_track_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) {
|
pub fn add_track_cb<F: Fn(usize) + 'static>(&self, cb: F) {
|
||||||
self.track_cbs.borrow_mut().push(Box::new(cb));
|
self.track_cbs.borrow_mut().push(Box::new(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,14 +160,10 @@ impl Player {
|
||||||
self.raise_cb.replace(Some(Box::new(cb)));
|
self.raise_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_playlist(&self) -> Vec<PlaylistItem> {
|
pub fn get_playlist(&self) -> Vec<Track> {
|
||||||
self.playlist.borrow().clone()
|
self.playlist.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_item(&self) -> Option<usize> {
|
|
||||||
self.current_item.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current_track(&self) -> Option<usize> {
|
pub fn get_current_track(&self) -> Option<usize> {
|
||||||
self.current_track.get()
|
self.current_track.get()
|
||||||
}
|
}
|
||||||
|
|
@ -188,10 +176,7 @@ impl Player {
|
||||||
self.playing.get()
|
self.playing.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
|
pub fn add_item(&self, item: Track) -> Result<()> {
|
||||||
if item.indices.is_empty() {
|
|
||||||
Err(Error::Other(String::from("Tried to add an empty playlist item!")))
|
|
||||||
} else {
|
|
||||||
let was_empty = {
|
let was_empty = {
|
||||||
let mut playlist = self.playlist.borrow_mut();
|
let mut playlist = self.playlist.borrow_mut();
|
||||||
let was_empty = playlist.is_empty();
|
let was_empty = playlist.is_empty();
|
||||||
|
|
@ -206,7 +191,7 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
if was_empty {
|
if was_empty {
|
||||||
self.set_track(0, 0)?;
|
self.set_track(0)?;
|
||||||
self.player.play();
|
self.player.play();
|
||||||
self.playing.set(true);
|
self.playing.set(true);
|
||||||
|
|
||||||
|
|
@ -223,7 +208,6 @@ impl Player {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play_pause(&self) {
|
pub fn play_pause(&self) {
|
||||||
if self.is_playing() {
|
if self.is_playing() {
|
||||||
|
|
@ -254,79 +238,56 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_previous(&self) -> bool {
|
pub fn has_previous(&self) -> bool {
|
||||||
if let Some(current_item) = self.current_item.get() {
|
|
||||||
if let Some(current_track) = self.current_track.get() {
|
if let Some(current_track) = self.current_track.get() {
|
||||||
current_track > 0 || current_item > 0
|
current_track > 0
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous(&self) -> Result<()> {
|
pub fn previous(&self) -> Result<()> {
|
||||||
let mut current_item = self.current_item.get()
|
|
||||||
.ok_or(Error::Other(String::from("Player tried to access non existant current item.")))?;
|
|
||||||
|
|
||||||
let mut current_track = self
|
let mut current_track = self
|
||||||
.current_track
|
.current_track
|
||||||
.get()
|
.get()
|
||||||
.ok_or(Error::Other(String::from("Player tried to access non existant current track.")))?;
|
.ok_or(Error::Other(String::from("Player tried to access non existant current track.")))?;
|
||||||
|
|
||||||
let playlist = self.playlist.borrow();
|
|
||||||
if current_track > 0 {
|
if current_track > 0 {
|
||||||
current_track -= 1;
|
current_track -= 1;
|
||||||
} else if current_item > 0 {
|
|
||||||
current_item -= 1;
|
|
||||||
current_track = playlist[current_item].indices.len() - 1;
|
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::Other(String::from("No existing previous track.")));
|
return Err(Error::Other(String::from("No existing previous track.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_track(current_item, current_track)
|
self.set_track(current_track)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_next(&self) -> bool {
|
pub fn has_next(&self) -> bool {
|
||||||
if let Some(current_item) = self.current_item.get() {
|
|
||||||
if let Some(current_track) = self.current_track.get() {
|
if let Some(current_track) = self.current_track.get() {
|
||||||
let playlist = self.playlist.borrow();
|
let playlist = self.playlist.borrow();
|
||||||
let item = &playlist[current_item];
|
current_track + 1 < playlist.len()
|
||||||
|
|
||||||
current_track + 1 < item.indices.len() || current_item + 1 < playlist.len()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&self) -> Result<()> {
|
pub fn next(&self) -> Result<()> {
|
||||||
let mut current_item = self.current_item.get()
|
|
||||||
.ok_or(Error::Other(String::from("Player tried to access non existant current item.")))?;
|
|
||||||
let mut current_track = self
|
let mut current_track = self
|
||||||
.current_track
|
.current_track
|
||||||
.get()
|
.get()
|
||||||
.ok_or(Error::Other(String::from("Player tried to access non existant current track.")))?;
|
.ok_or(Error::Other(String::from("Player tried to access non existant current track.")))?;
|
||||||
|
|
||||||
let playlist = self.playlist.borrow();
|
let playlist = self.playlist.borrow();
|
||||||
let item = &playlist[current_item];
|
|
||||||
if current_track + 1 < item.indices.len() {
|
if current_track + 1 < playlist.len() {
|
||||||
current_track += 1;
|
current_track += 1;
|
||||||
} else if current_item + 1 < playlist.len() {
|
|
||||||
current_item += 1;
|
|
||||||
current_track = 0;
|
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::Other(String::from("No existing previous track.")));
|
return Err(Error::Other(String::from("No existing next track.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_track(current_item, current_track)
|
self.set_track(current_track)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_track(&self, current_item: usize, current_track: usize) -> Result<()> {
|
pub fn set_track(&self, current_track: usize) -> Result<()> {
|
||||||
let item = &self.playlist.borrow()[current_item];
|
let track = &self.playlist.borrow()[current_track];
|
||||||
let track = &item.track_set.tracks[current_track];
|
|
||||||
|
|
||||||
let path = self.music_library_path.join(track.path.clone())
|
let path = self.music_library_path.join(track.path.clone())
|
||||||
.into_os_string().into_string().unwrap();
|
.into_os_string().into_string().unwrap();
|
||||||
|
|
@ -340,26 +301,25 @@ impl Player {
|
||||||
self.player.play();
|
self.player.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.current_item.set(Some(current_item));
|
|
||||||
self.current_track.set(Some(current_track));
|
self.current_track.set(Some(current_track));
|
||||||
|
|
||||||
for cb in &*self.track_cbs.borrow() {
|
for cb in &*self.track_cbs.borrow() {
|
||||||
cb(current_item, current_track);
|
cb(current_track);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
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.track_set.recording.work.parts[*part].title.clone());
|
parts.push(track.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = item.track_set.recording.work.get_title();
|
let mut title = track.recording.work.get_title();
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
title = format!("{}: {}", title, parts.join(", "));
|
title = format!("{}: {}", title, parts.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitle = item.track_set.recording.get_performers();
|
let subtitle = track.recording.get_performers();
|
||||||
|
|
||||||
let mut metadata = Metadata::new();
|
let mut metadata = Metadata::new();
|
||||||
metadata.artist = Some(vec![title]);
|
metadata.artist = Some(vec![title]);
|
||||||
|
|
@ -379,7 +339,7 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
for cb in &*self.track_cbs.borrow() {
|
for cb in &*self.track_cbs.borrow() {
|
||||||
cb(self.current_item.get().unwrap(), self.current_track.get().unwrap());
|
cb(self.current_track.get().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
for cb in &*self.duration_cbs.borrow() {
|
for cb in &*self.duration_cbs.borrow() {
|
||||||
|
|
@ -394,7 +354,6 @@ impl Player {
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
self.player.stop();
|
self.player.stop();
|
||||||
self.playing.set(false);
|
self.playing.set(false);
|
||||||
self.current_item.set(None);
|
|
||||||
self.current_track.set(None);
|
self.current_track.set(None);
|
||||||
self.playlist.replace(Vec::new());
|
self.playlist.replace(Vec::new());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,5 @@ DROP TABLE "ensembles";
|
||||||
DROP TABLE "recordings";
|
DROP TABLE "recordings";
|
||||||
DROP TABLE "performances";
|
DROP TABLE "performances";
|
||||||
DROP TABLE "mediums";
|
DROP TABLE "mediums";
|
||||||
DROP TABLE "track_sets";
|
|
||||||
DROP TABLE "tracks";
|
DROP TABLE "tracks";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,17 +60,11 @@ CREATE TABLE "mediums" (
|
||||||
"discid" TEXT
|
"discid" TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE "track_sets" (
|
CREATE TABLE "tracks" (
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
"medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE,
|
"medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE,
|
||||||
"index" INTEGER NOT NULL,
|
"index" INTEGER NOT NULL,
|
||||||
"recording" TEXT NOT NULL REFERENCES "recordings"("id")
|
"recording" TEXT NOT NULL REFERENCES "recordings"("id"),
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE "tracks" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"track_set" TEXT NOT NULL REFERENCES "track_sets"("id") ON DELETE CASCADE,
|
|
||||||
"index" INTEGER NOT NULL,
|
|
||||||
"work_parts" TEXT NOT NULL,
|
"work_parts" TEXT NOT NULL,
|
||||||
"path" TEXT NOT NULL
|
"path" TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::generate_id;
|
use super::generate_id;
|
||||||
use super::schema::{ensembles, mediums, performances, persons, recordings, track_sets, tracks};
|
use super::schema::{ensembles, mediums, performances, persons, recordings, tracks};
|
||||||
use super::{Database, Error, Recording, Result};
|
use super::{Database, Error, Recording, Result};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -18,32 +18,17 @@ pub struct Medium {
|
||||||
/// If applicable, the MusicBrainz DiscID.
|
/// If applicable, the MusicBrainz DiscID.
|
||||||
pub discid: Option<String>,
|
pub discid: Option<String>,
|
||||||
|
|
||||||
/// The tracks of the medium, grouped by recording.
|
/// The tracks of the medium.
|
||||||
pub tracks: Vec<TrackSet>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Medium {
|
|
||||||
/// Get an iterator that iterates through all tracks within this medium in order.
|
|
||||||
pub fn track_iter<'a>(&'a self) -> TrackIter<'a> {
|
|
||||||
TrackIter::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A set of tracks of one recording within a medium.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct TrackSet {
|
|
||||||
/// The recording to which the tracks belong.
|
|
||||||
pub recording: Recording,
|
|
||||||
|
|
||||||
/// The actual tracks.
|
|
||||||
pub tracks: Vec<Track>,
|
pub tracks: Vec<Track>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A track within a recording on a medium.
|
/// A track on a medium.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
|
/// The recording on this track.
|
||||||
|
pub recording: Recording,
|
||||||
|
|
||||||
/// The work parts that are played on this track. They are indices to the
|
/// The work parts that are played on this track. They are indices to the
|
||||||
/// work parts of the work that is associated with the recording.
|
/// work parts of the work that is associated with the recording.
|
||||||
pub work_parts: Vec<usize>,
|
pub work_parts: Vec<usize>,
|
||||||
|
|
@ -63,23 +48,14 @@ struct MediumRow {
|
||||||
pub discid: Option<String>,
|
pub discid: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Table data for a [`TrackSet`].
|
|
||||||
#[derive(Insertable, Queryable, Debug, Clone)]
|
|
||||||
#[table_name = "track_sets"]
|
|
||||||
struct TrackSetRow {
|
|
||||||
pub id: String,
|
|
||||||
pub medium: String,
|
|
||||||
pub index: i32,
|
|
||||||
pub recording: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Table data for a [`Track`].
|
/// Table data for a [`Track`].
|
||||||
#[derive(Insertable, Queryable, Debug, Clone)]
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
#[table_name = "tracks"]
|
#[table_name = "tracks"]
|
||||||
struct TrackRow {
|
struct TrackRow {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub track_set: String,
|
pub medium: String,
|
||||||
pub index: i32,
|
pub index: i32,
|
||||||
|
pub recording: String,
|
||||||
pub work_parts: String,
|
pub work_parts: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +111,7 @@ impl Database {
|
||||||
self.connection.transaction::<(), Error, _>(|| {
|
self.connection.transaction::<(), Error, _>(|| {
|
||||||
let medium_id = &medium.id;
|
let medium_id = &medium.id;
|
||||||
|
|
||||||
// This will also delete the track sets and tracks.
|
// This will also delete the tracks.
|
||||||
self.delete_medium(medium_id)?;
|
self.delete_medium(medium_id)?;
|
||||||
|
|
||||||
// Add the new medium.
|
// Add the new medium.
|
||||||
|
|
@ -150,30 +126,15 @@ impl Database {
|
||||||
.values(medium_row)
|
.values(medium_row)
|
||||||
.execute(&self.connection)?;
|
.execute(&self.connection)?;
|
||||||
|
|
||||||
for (index, track_set) in medium.tracks.iter().enumerate() {
|
for (index, track) in medium.tracks.iter().enumerate() {
|
||||||
// Add associated items from the server, if they don't already
|
// Add associated items from the server, if they don't already exist.
|
||||||
// exist.
|
|
||||||
|
|
||||||
if self.get_recording(&track_set.recording.id)?.is_none() {
|
if self.get_recording(&track.recording.id)?.is_none() {
|
||||||
self.update_recording(track_set.recording.clone())?;
|
self.update_recording(track.recording.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the actual track set data.
|
// Add the actual track data.
|
||||||
|
|
||||||
let track_set_id = generate_id();
|
|
||||||
|
|
||||||
let track_set_row = TrackSetRow {
|
|
||||||
id: track_set_id.clone(),
|
|
||||||
medium: medium_id.to_owned(),
|
|
||||||
index: index as i32,
|
|
||||||
recording: track_set.recording.id.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(track_sets::table)
|
|
||||||
.values(track_set_row)
|
|
||||||
.execute(&self.connection)?;
|
|
||||||
|
|
||||||
for (index, track) in track_set.tracks.iter().enumerate() {
|
|
||||||
let work_parts = track
|
let work_parts = track
|
||||||
.work_parts
|
.work_parts
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -183,8 +144,9 @@ impl Database {
|
||||||
|
|
||||||
let track_row = TrackRow {
|
let track_row = TrackRow {
|
||||||
id: generate_id(),
|
id: generate_id(),
|
||||||
track_set: track_set_id.clone(),
|
medium: medium_id.to_owned(),
|
||||||
index: index as i32,
|
index: index as i32,
|
||||||
|
recording: track.recording.id.clone(),
|
||||||
work_parts,
|
work_parts,
|
||||||
path: track.path.clone(),
|
path: track.path.clone(),
|
||||||
};
|
};
|
||||||
|
|
@ -193,7 +155,6 @@ impl Database {
|
||||||
.values(track_row)
|
.values(track_row)
|
||||||
.execute(&self.connection)?;
|
.execute(&self.connection)?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -238,8 +199,8 @@ impl Database {
|
||||||
let mut mediums: Vec<Medium> = Vec::new();
|
let mut mediums: Vec<Medium> = Vec::new();
|
||||||
|
|
||||||
let rows = mediums::table
|
let rows = mediums::table
|
||||||
.inner_join(track_sets::table.on(track_sets::medium.eq(mediums::id)))
|
.inner_join(tracks::table.on(tracks::medium.eq(mediums::id)))
|
||||||
.inner_join(recordings::table.on(recordings::id.eq(track_sets::recording)))
|
.inner_join(recordings::table.on(recordings::id.eq(tracks::recording)))
|
||||||
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
|
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
|
||||||
.inner_join(persons::table.on(persons::id.nullable().eq(performances::person)))
|
.inner_join(persons::table.on(persons::id.nullable().eq(performances::person)))
|
||||||
.filter(persons::id.eq(person_id))
|
.filter(persons::id.eq(person_id))
|
||||||
|
|
@ -260,8 +221,8 @@ impl Database {
|
||||||
let mut mediums: Vec<Medium> = Vec::new();
|
let mut mediums: Vec<Medium> = Vec::new();
|
||||||
|
|
||||||
let rows = mediums::table
|
let rows = mediums::table
|
||||||
.inner_join(track_sets::table.on(track_sets::medium.eq(mediums::id)))
|
.inner_join(tracks::table.on(tracks::medium.eq(tracks::id)))
|
||||||
.inner_join(recordings::table.on(recordings::id.eq(track_sets::recording)))
|
.inner_join(recordings::table.on(recordings::id.eq(tracks::recording)))
|
||||||
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
|
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
|
||||||
.inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble)))
|
.inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble)))
|
||||||
.filter(ensembles::id.eq(ensemble_id))
|
.filter(ensembles::id.eq(ensemble_id))
|
||||||
|
|
@ -284,93 +245,81 @@ impl Database {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all available track sets for a recording.
|
/// Get all available tracks for a recording.
|
||||||
pub fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
|
pub fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> {
|
||||||
let mut track_sets: Vec<TrackSet> = Vec::new();
|
let mut tracks: Vec<Track> = Vec::new();
|
||||||
|
|
||||||
let rows = track_sets::table
|
let rows = tracks::table
|
||||||
.inner_join(recordings::table.on(recordings::id.eq(track_sets::recording)))
|
.inner_join(recordings::table.on(recordings::id.eq(tracks::recording)))
|
||||||
.filter(recordings::id.eq(recording_id))
|
.filter(recordings::id.eq(recording_id))
|
||||||
.select(track_sets::table::all_columns())
|
.select(tracks::table::all_columns())
|
||||||
.load::<TrackSetRow>(&self.connection)?;
|
.load::<TrackRow>(&self.connection)?;
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let track_set = self.get_track_set_from_row(row)?;
|
let track = self.get_track_from_row(row)?;
|
||||||
track_sets.push(track_set);
|
tracks.push(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(track_sets)
|
Ok(tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve all available information on a medium from related tables.
|
/// Retrieve all available information on a medium from related tables.
|
||||||
fn get_medium_data(&self, row: MediumRow) -> Result<Medium> {
|
fn get_medium_data(&self, row: MediumRow) -> Result<Medium> {
|
||||||
let track_set_rows = track_sets::table
|
|
||||||
.filter(track_sets::medium.eq(&row.id))
|
|
||||||
.order_by(track_sets::index)
|
|
||||||
.load::<TrackSetRow>(&self.connection)?;
|
|
||||||
|
|
||||||
let mut track_sets = Vec::new();
|
|
||||||
|
|
||||||
for track_set_row in track_set_rows {
|
|
||||||
let track_set = self.get_track_set_from_row(track_set_row)?;
|
|
||||||
track_sets.push(track_set);
|
|
||||||
}
|
|
||||||
|
|
||||||
let medium = Medium {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
discid: row.discid,
|
|
||||||
tracks: track_sets,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(medium)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a track set row from the database to an actual track set.
|
|
||||||
fn get_track_set_from_row(&self, row: TrackSetRow) -> Result<TrackSet> {
|
|
||||||
let recording_id = row.recording;
|
|
||||||
|
|
||||||
let recording = self
|
|
||||||
.get_recording(&recording_id)?
|
|
||||||
.ok_or(Error::Other(format!(
|
|
||||||
"Failed to get recording ({}) for track set ({}).",
|
|
||||||
recording_id,
|
|
||||||
row.id,
|
|
||||||
)))?;
|
|
||||||
|
|
||||||
let track_rows = tracks::table
|
let track_rows = tracks::table
|
||||||
.filter(tracks::track_set.eq(row.id))
|
.filter(tracks::medium.eq(&row.id))
|
||||||
.order_by(tracks::index)
|
.order_by(tracks::index)
|
||||||
.load::<TrackRow>(&self.connection)?;
|
.load::<TrackRow>(&self.connection)?;
|
||||||
|
|
||||||
let mut tracks = Vec::new();
|
let mut tracks = Vec::new();
|
||||||
|
|
||||||
for track_row in track_rows {
|
for track_row in track_rows {
|
||||||
|
let track = self.get_track_from_row(track_row)?;
|
||||||
|
tracks.push(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
let medium = Medium {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
discid: row.discid,
|
||||||
|
tracks,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a track row from the database to an actual track.
|
||||||
|
fn get_track_from_row(&self, row: TrackRow) -> Result<Track> {
|
||||||
|
let recording_id = row.recording;
|
||||||
|
|
||||||
|
let recording = self
|
||||||
|
.get_recording(&recording_id)?
|
||||||
|
.ok_or(Error::Other(format!(
|
||||||
|
"Failed to get recording ({}) for track ({}).",
|
||||||
|
recording_id,
|
||||||
|
row.id,
|
||||||
|
)))?;
|
||||||
|
|
||||||
let mut part_indices = Vec::new();
|
let mut part_indices = Vec::new();
|
||||||
|
|
||||||
let work_parts = track_row
|
let work_parts = row
|
||||||
.work_parts
|
.work_parts
|
||||||
.split(',');
|
.split(',');
|
||||||
|
|
||||||
for part_index in work_parts {
|
for part_index in work_parts {
|
||||||
if !part_index.is_empty() {
|
if !part_index.is_empty() {
|
||||||
let index = str::parse(part_index)
|
let index = str::parse(part_index)
|
||||||
.or(Err(Error::Other(format!("Failed to parse part index from '{}'.", track_row.work_parts))))?;
|
.or(Err(Error::Other(format!("Failed to parse part index from '{}'.", row.work_parts))))?;
|
||||||
|
|
||||||
part_indices.push(index);
|
part_indices.push(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let track = Track {
|
let track = Track {
|
||||||
|
recording,
|
||||||
work_parts: part_indices,
|
work_parts: part_indices,
|
||||||
path: track_row.path,
|
path: row.path,
|
||||||
};
|
};
|
||||||
|
|
||||||
tracks.push(track);
|
Ok(track)
|
||||||
}
|
|
||||||
|
|
||||||
let track_set = TrackSet { recording, tracks };
|
|
||||||
|
|
||||||
Ok(track_set)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,19 +55,11 @@ table! {
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
track_sets (id) {
|
tracks (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
medium -> Text,
|
medium -> Text,
|
||||||
index -> Integer,
|
index -> Integer,
|
||||||
recording -> Text,
|
recording -> Text,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
tracks (id) {
|
|
||||||
id -> Text,
|
|
||||||
track_set -> Text,
|
|
||||||
index -> Integer,
|
|
||||||
work_parts -> Text,
|
work_parts -> Text,
|
||||||
path -> Text,
|
path -> Text,
|
||||||
}
|
}
|
||||||
|
|
@ -106,9 +98,8 @@ joinable!(performances -> instruments (role));
|
||||||
joinable!(performances -> persons (person));
|
joinable!(performances -> persons (person));
|
||||||
joinable!(performances -> recordings (recording));
|
joinable!(performances -> recordings (recording));
|
||||||
joinable!(recordings -> works (work));
|
joinable!(recordings -> works (work));
|
||||||
joinable!(track_sets -> mediums (medium));
|
joinable!(tracks -> mediums (medium));
|
||||||
joinable!(track_sets -> recordings (recording));
|
joinable!(tracks -> recordings (recording));
|
||||||
joinable!(tracks -> track_sets (track_set));
|
|
||||||
joinable!(work_parts -> works (work));
|
joinable!(work_parts -> works (work));
|
||||||
joinable!(work_sections -> works (work));
|
joinable!(work_sections -> works (work));
|
||||||
joinable!(works -> persons (composer));
|
joinable!(works -> persons (composer));
|
||||||
|
|
@ -121,7 +112,6 @@ allow_tables_to_appear_in_same_query!(
|
||||||
performances,
|
performances,
|
||||||
persons,
|
persons,
|
||||||
recordings,
|
recordings,
|
||||||
track_sets,
|
|
||||||
tracks,
|
tracks,
|
||||||
work_parts,
|
work_parts,
|
||||||
work_sections,
|
work_sections,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ pub enum Action {
|
||||||
GetMediumsForPerson(String, Sender<Result<Vec<Medium>>>),
|
GetMediumsForPerson(String, Sender<Result<Vec<Medium>>>),
|
||||||
GetMediumsForEnsemble(String, Sender<Result<Vec<Medium>>>),
|
GetMediumsForEnsemble(String, Sender<Result<Vec<Medium>>>),
|
||||||
DeleteMedium(String, Sender<Result<()>>),
|
DeleteMedium(String, Sender<Result<()>>),
|
||||||
GetTrackSets(String, Sender<Result<Vec<TrackSet>>>),
|
GetTracks(String, Sender<Result<Vec<Track>>>),
|
||||||
Stop(Sender<()>),
|
Stop(Sender<()>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,8 +145,8 @@ impl DbThread {
|
||||||
DeleteMedium(id, sender) => {
|
DeleteMedium(id, sender) => {
|
||||||
sender.send(db.delete_medium(&id)).unwrap();
|
sender.send(db.delete_medium(&id)).unwrap();
|
||||||
}
|
}
|
||||||
GetTrackSets(recording_id, sender) => {
|
GetTracks(recording_id, sender) => {
|
||||||
sender.send(db.get_track_sets(&recording_id)).unwrap();
|
sender.send(db.get_tracks(&recording_id)).unwrap();
|
||||||
}
|
}
|
||||||
Stop(sender) => {
|
Stop(sender) => {
|
||||||
sender.send(()).unwrap();
|
sender.send(()).unwrap();
|
||||||
|
|
@ -371,10 +371,10 @@ impl DbThread {
|
||||||
receiver.await?
|
receiver.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all track sets for a recording.
|
/// Get all tracks for a recording.
|
||||||
pub async fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
|
pub async fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> {
|
||||||
let (sender, receiver) = oneshot::channel();
|
let (sender, receiver) = oneshot::channel();
|
||||||
self.action_sender.send(GetTrackSets(recording_id.to_owned(), sender))?;
|
self.action_sender.send(GetTracks(recording_id.to_owned(), sender))?;
|
||||||
receiver.await?
|
receiver.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use glib::prelude::*;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk_macros::get_widget;
|
use gtk_macros::get_widget;
|
||||||
use libadwaita::prelude::*;
|
use libadwaita::prelude::*;
|
||||||
use musicus_backend::db::{generate_id, Medium, Track, TrackSet};
|
use musicus_backend::db::{generate_id, Medium};
|
||||||
use musicus_backend::import::ImportSession;
|
use musicus_backend::import::ImportSession;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
@ -24,7 +24,7 @@ pub struct MediumEditor {
|
||||||
status_page: libadwaita::StatusPage,
|
status_page: libadwaita::StatusPage,
|
||||||
disc_status_page: libadwaita::StatusPage,
|
disc_status_page: libadwaita::StatusPage,
|
||||||
track_set_list: Rc<List>,
|
track_set_list: Rc<List>,
|
||||||
track_sets: RefCell<Vec<TrackSetData>>,
|
// track_sets: RefCell<Vec<TrackSetData>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screen<Arc<ImportSession>, Medium> for MediumEditor {
|
impl Screen<Arc<ImportSession>, Medium> for MediumEditor {
|
||||||
|
|
@ -59,7 +59,7 @@ impl Screen<Arc<ImportSession>, Medium> for MediumEditor {
|
||||||
status_page,
|
status_page,
|
||||||
disc_status_page,
|
disc_status_page,
|
||||||
track_set_list: list,
|
track_set_list: list,
|
||||||
track_sets: RefCell::new(Vec::new()),
|
// track_sets: RefCell::new(Vec::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect signals and callbacks
|
// Connect signals and callbacks
|
||||||
|
|
@ -83,43 +83,43 @@ impl Screen<Arc<ImportSession>, Medium> for MediumEditor {
|
||||||
|
|
||||||
add_button.connect_clicked(clone!(@weak this => move |_| {
|
add_button.connect_clicked(clone!(@weak this => move |_| {
|
||||||
spawn!(@clone this, async move {
|
spawn!(@clone this, async move {
|
||||||
if let Some(track_set) = push!(this.handle, TrackSetEditor, Arc::clone(&this.session)).await {
|
// if let Some(track_set) = push!(this.handle, TrackSetEditor, Arc::clone(&this.session)).await {
|
||||||
let length = {
|
// let length = {
|
||||||
let mut track_sets = this.track_sets.borrow_mut();
|
// let mut track_sets = this.track_sets.borrow_mut();
|
||||||
track_sets.push(track_set);
|
// track_sets.push(track_set);
|
||||||
track_sets.len()
|
// track_sets.len()
|
||||||
};
|
// };
|
||||||
|
|
||||||
this.track_set_list.update(length);
|
// this.track_set_list.update(length);
|
||||||
}
|
// }
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.track_set_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
// this.track_set_list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||||
let track_set = &this.track_sets.borrow()[index];
|
// let track_set = &this.track_sets.borrow()[index];
|
||||||
|
|
||||||
let title = track_set.recording.work.get_title();
|
// let title = track_set.recording.work.get_title();
|
||||||
let subtitle = track_set.recording.get_performers();
|
// let subtitle = track_set.recording.get_performers();
|
||||||
|
|
||||||
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
|
// let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
|
||||||
let edit_button = gtk::Button::new();
|
// let edit_button = gtk::Button::new();
|
||||||
edit_button.set_has_frame(false);
|
// edit_button.set_has_frame(false);
|
||||||
edit_button.set_valign(gtk::Align::Center);
|
// edit_button.set_valign(gtk::Align::Center);
|
||||||
edit_button.set_child(Some(&edit_image));
|
// edit_button.set_child(Some(&edit_image));
|
||||||
|
|
||||||
let row = libadwaita::ActionRow::new();
|
// let row = libadwaita::ActionRow::new();
|
||||||
row.set_activatable(true);
|
// row.set_activatable(true);
|
||||||
row.set_title(Some(&title));
|
// row.set_title(Some(&title));
|
||||||
row.set_subtitle(Some(&subtitle));
|
// row.set_subtitle(Some(&subtitle));
|
||||||
row.add_suffix(&edit_button);
|
// row.add_suffix(&edit_button);
|
||||||
row.set_activatable_widget(Some(&edit_button));
|
// row.set_activatable_widget(Some(&edit_button));
|
||||||
|
|
||||||
edit_button.connect_clicked(clone!(@weak this => move |_| {
|
// edit_button.connect_clicked(clone!(@weak this => move |_| {
|
||||||
// TODO: Implement editing.
|
// TODO: Implement editing.
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
row.upcast()
|
// row.upcast()
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
try_again_button.connect_clicked(clone!(@weak this => move |_| {
|
try_again_button.connect_clicked(clone!(@weak this => move |_| {
|
||||||
this.widget.set_visible_child_name("content");
|
this.widget.set_visible_child_name("content");
|
||||||
|
|
@ -136,68 +136,68 @@ impl Screen<Arc<ImportSession>, Medium> for MediumEditor {
|
||||||
impl MediumEditor {
|
impl MediumEditor {
|
||||||
/// Save the medium and possibly upload it to the server.
|
/// Save the medium and possibly upload it to the server.
|
||||||
async fn save(&self) -> Result<Medium> {
|
async fn save(&self) -> Result<Medium> {
|
||||||
let name = self.name_entry.get_text().to_string();
|
// let name = self.name_entry.get_text().to_string();
|
||||||
|
|
||||||
// Create a new directory in the music library path for the imported medium.
|
// Create a new directory in the music library path for the imported medium.
|
||||||
|
|
||||||
let mut path = self.handle.backend.get_music_library_path().unwrap().clone();
|
// let mut path = self.handle.backend.get_music_library_path().unwrap().clone();
|
||||||
path.push(&name);
|
// path.push(&name);
|
||||||
std::fs::create_dir(&path)?;
|
// std::fs::create_dir(&path)?;
|
||||||
|
|
||||||
// Convert the track set data to real track sets.
|
// Convert the track set data to real track sets.
|
||||||
|
|
||||||
let mut track_sets = Vec::new();
|
// let mut track_sets = Vec::new();
|
||||||
let import_tracks = self.session.tracks();
|
// let import_tracks = self.session.tracks();
|
||||||
|
|
||||||
for track_set_data in &*self.track_sets.borrow() {
|
// for track_set_data in &*self.track_sets.borrow() {
|
||||||
let mut tracks = Vec::new();
|
// let mut tracks = Vec::new();
|
||||||
|
|
||||||
for track_data in &track_set_data.tracks {
|
// for track_data in &track_set_data.tracks {
|
||||||
// Copy the corresponding audio file to the music library.
|
// Copy the corresponding audio file to the music library.
|
||||||
|
|
||||||
let import_track = &import_tracks[track_data.track_source];
|
// let import_track = &import_tracks[track_data.track_source];
|
||||||
|
|
||||||
let mut track_path = path.clone();
|
// let mut track_path = path.clone();
|
||||||
track_path.push(import_track.path.file_name().unwrap());
|
// track_path.push(import_track.path.file_name().unwrap());
|
||||||
|
|
||||||
std::fs::copy(&import_track.path, &track_path)?;
|
// std::fs::copy(&import_track.path, &track_path)?;
|
||||||
|
|
||||||
// Create the real track.
|
// Create the real track.
|
||||||
|
|
||||||
let track = Track {
|
// let track = Track {
|
||||||
work_parts: track_data.work_parts.clone(),
|
// work_parts: track_data.work_parts.clone(),
|
||||||
path: track_path.to_str().unwrap().to_owned(),
|
// path: track_path.to_str().unwrap().to_owned(),
|
||||||
};
|
// };
|
||||||
|
|
||||||
tracks.push(track);
|
// tracks.push(track);
|
||||||
}
|
// }
|
||||||
|
|
||||||
let track_set = TrackSet {
|
// let track_set = TrackSet {
|
||||||
recording: track_set_data.recording.clone(),
|
// recording: track_set_data.recording.clone(),
|
||||||
tracks,
|
// tracks,
|
||||||
};
|
// };
|
||||||
|
|
||||||
track_sets.push(track_set);
|
// track_sets.push(track_set);
|
||||||
}
|
// }
|
||||||
|
|
||||||
let medium = Medium {
|
// let medium = Medium {
|
||||||
id: generate_id(),
|
// id: generate_id(),
|
||||||
name: self.name_entry.get_text().to_string(),
|
// name: self.name_entry.get_text().to_string(),
|
||||||
discid: Some(self.session.source_id().to_owned()),
|
// discid: Some(self.session.source_id().to_owned()),
|
||||||
tracks: track_sets,
|
// tracks: track_sets,
|
||||||
};
|
// };
|
||||||
|
|
||||||
let upload = self.publish_switch.get_active();
|
// let upload = self.publish_switch.get_active();
|
||||||
if upload {
|
// if upload {
|
||||||
self.handle.backend.cl().post_medium(&medium).await?;
|
// self.handle.backend.cl().post_medium(&medium).await?;
|
||||||
}
|
// }
|
||||||
|
|
||||||
self.handle.backend
|
// self.handle.backend
|
||||||
.db()
|
// .db()
|
||||||
.update_medium(medium.clone())
|
// .update_medium(medium.clone())
|
||||||
.await?;
|
// .await?;
|
||||||
|
|
||||||
self.handle.backend.library_changed();
|
// self.handle.backend.library_changed();
|
||||||
|
|
||||||
Ok(medium)
|
Ok(medium)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,53 +5,21 @@ use gettextrs::gettext;
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use libadwaita::prelude::*;
|
use libadwaita::prelude::*;
|
||||||
use musicus_backend::PlaylistItem;
|
|
||||||
use musicus_backend::db::Medium;
|
use musicus_backend::db::Medium;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// Elements for visually representing the contents of the medium.
|
|
||||||
enum ListItem {
|
|
||||||
/// A header shown on top of a track set. The value is the index of the corresponding track set
|
|
||||||
/// within the medium.
|
|
||||||
Header(usize),
|
|
||||||
|
|
||||||
/// A track. The indices are from the track set and the track.
|
|
||||||
Track(usize, usize),
|
|
||||||
|
|
||||||
/// A separator shown between track sets.
|
|
||||||
Separator,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A screen for showing the contents of a medium.
|
/// A screen for showing the contents of a medium.
|
||||||
pub struct MediumScreen {
|
pub struct MediumScreen {
|
||||||
handle: NavigationHandle<()>,
|
handle: NavigationHandle<()>,
|
||||||
medium: Medium,
|
medium: Medium,
|
||||||
widget: widgets::Screen,
|
widget: widgets::Screen,
|
||||||
list: Rc<List>,
|
list: Rc<List>,
|
||||||
items: Vec<ListItem>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screen<Medium, ()> for MediumScreen {
|
impl Screen<Medium, ()> for MediumScreen {
|
||||||
/// Create a new medium screen for the specified medium and load the
|
/// Create a new medium screen for the specified medium and load the
|
||||||
/// contents asynchronously.
|
/// contents asynchronously.
|
||||||
fn new(medium: Medium, handle: NavigationHandle<()>) -> Rc<Self> {
|
fn new(medium: Medium, handle: NavigationHandle<()>) -> Rc<Self> {
|
||||||
let mut items = Vec::new();
|
|
||||||
let mut first = true;
|
|
||||||
|
|
||||||
for (track_set_index, track_set) in medium.tracks.iter().enumerate() {
|
|
||||||
if !first {
|
|
||||||
items.push(ListItem::Separator);
|
|
||||||
} else {
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push(ListItem::Header(track_set_index));
|
|
||||||
|
|
||||||
for (track_index, _) in track_set.tracks.iter().enumerate() {
|
|
||||||
items.push(ListItem::Track(track_set_index, track_index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let widget = widgets::Screen::new();
|
let widget = widgets::Screen::new();
|
||||||
widget.set_title(&medium.name);
|
widget.set_title(&medium.name);
|
||||||
|
|
||||||
|
|
@ -65,7 +33,6 @@ impl Screen<Medium, ()> for MediumScreen {
|
||||||
medium,
|
medium,
|
||||||
widget,
|
widget,
|
||||||
list,
|
list,
|
||||||
items,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.widget.set_back_cb(clone!(@weak this => move || {
|
this.widget.set_back_cb(clone!(@weak this => move || {
|
||||||
|
|
@ -82,39 +49,17 @@ impl Screen<Medium, ()> for MediumScreen {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
section.add_action("media-playback-start-symbolic", clone!(@weak this => move || {
|
section.add_action("media-playback-start-symbolic", clone!(@weak this => move || {
|
||||||
for track_set in &this.medium.tracks {
|
for track in &this.medium.tracks {
|
||||||
let indices = (0..track_set.tracks.len()).collect();
|
this.handle.backend.pl().add_item(track.clone()).unwrap();
|
||||||
|
|
||||||
let playlist_item = PlaylistItem {
|
|
||||||
track_set: track_set.clone(),
|
|
||||||
indices,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handle.backend.pl().add_item(playlist_item).unwrap();
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||||
match this.items[index] {
|
let track = &this.medium.tracks[index];
|
||||||
ListItem::Header(index) => {
|
|
||||||
let track_set = &this.medium.tracks[index];
|
|
||||||
let recording = &track_set.recording;
|
|
||||||
|
|
||||||
let row = libadwaita::ActionRow::new();
|
|
||||||
row.set_activatable(false);
|
|
||||||
row.set_selectable(false);
|
|
||||||
row.set_title(Some(&recording.work.get_title()));
|
|
||||||
row.set_subtitle(Some(&recording.get_performers()));
|
|
||||||
|
|
||||||
row.upcast()
|
|
||||||
}
|
|
||||||
ListItem::Track(track_set_index, track_index) => {
|
|
||||||
let track_set = &this.medium.tracks[track_set_index];
|
|
||||||
let track = &track_set.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(track_set.recording.work.parts[*part].title.clone());
|
parts.push(track.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = if parts.is_empty() {
|
let title = if parts.is_empty() {
|
||||||
|
|
@ -130,15 +75,9 @@ impl Screen<Medium, ()> for MediumScreen {
|
||||||
row.set_margin_start(12);
|
row.set_margin_start(12);
|
||||||
|
|
||||||
row.upcast()
|
row.upcast()
|
||||||
}
|
|
||||||
ListItem::Separator => {
|
|
||||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
|
||||||
separator.upcast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.list.update(this.items.len());
|
this.list.update(this.medium.tracks.len());
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,25 @@ use glib::clone;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk_macros::get_widget;
|
use gtk_macros::get_widget;
|
||||||
use libadwaita::prelude::*;
|
use libadwaita::prelude::*;
|
||||||
use musicus_backend::PlaylistItem;
|
use musicus_backend::db::Track;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// Elements for visually representing the playlist.
|
/// Elements for visually representing the playlist.
|
||||||
enum ListItem {
|
enum ListItem {
|
||||||
/// A header shown on top of a track set. This contains an index
|
/// A playable track.
|
||||||
/// referencing the playlist item containing this track set.
|
Track {
|
||||||
Header(usize),
|
/// Index within the playlist.
|
||||||
|
index: usize,
|
||||||
|
|
||||||
/// A playable track. This contains an index to the playlist item, an
|
/// Whether this is the first track of the recording.
|
||||||
/// index to the track and whether it is the currently played one.
|
first: bool,
|
||||||
Track(usize, usize, bool),
|
|
||||||
|
|
||||||
/// A separator shown between track sets.
|
/// Whether this is the currently played track.
|
||||||
|
playing: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A separator shown between recordings.
|
||||||
Separator,
|
Separator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,10 +41,9 @@ pub struct PlayerScreen {
|
||||||
play_image: gtk::Image,
|
play_image: gtk::Image,
|
||||||
pause_image: gtk::Image,
|
pause_image: gtk::Image,
|
||||||
list: Rc<List>,
|
list: Rc<List>,
|
||||||
playlist: RefCell<Vec<PlaylistItem>>,
|
playlist: RefCell<Vec<Track>>,
|
||||||
items: RefCell<Vec<ListItem>>,
|
items: RefCell<Vec<ListItem>>,
|
||||||
seeking: Cell<bool>,
|
seeking: Cell<bool>,
|
||||||
current_item: Cell<usize>,
|
|
||||||
current_track: Cell<usize>,
|
current_track: Cell<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +90,6 @@ impl Screen<(), ()> for PlayerScreen {
|
||||||
items: RefCell::new(Vec::new()),
|
items: RefCell::new(Vec::new()),
|
||||||
playlist: RefCell::new(Vec::new()),
|
playlist: RefCell::new(Vec::new()),
|
||||||
seeking: Cell::new(false),
|
seeking: Cell::new(false),
|
||||||
current_item: Cell::new(0),
|
|
||||||
current_track: Cell::new(0),
|
current_track: Cell::new(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -102,28 +104,26 @@ impl Screen<(), ()> for PlayerScreen {
|
||||||
this.show_playlist();
|
this.show_playlist();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
player.add_track_cb(clone!(@weak this, @weak player => @default-return (), move |current_item, current_track| {
|
player.add_track_cb(clone!(@weak this, @weak player => @default-return (), move |current_track| {
|
||||||
this.previous_button.set_sensitive(this.handle.backend.pl().has_previous());
|
this.previous_button.set_sensitive(this.handle.backend.pl().has_previous());
|
||||||
this.next_button.set_sensitive(this.handle.backend.pl().has_next());
|
this.next_button.set_sensitive(this.handle.backend.pl().has_next());
|
||||||
|
|
||||||
let item = &this.playlist.borrow()[current_item];
|
let track = &this.playlist.borrow()[current_track];
|
||||||
let track = &item.track_set.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.track_set.recording.work.parts[*part].title.clone());
|
parts.push(track.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = item.track_set.recording.work.get_title();
|
let mut title = track.recording.work.get_title();
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
title = format!("{}: {}", title, parts.join(", "));
|
title = format!("{}: {}", title, parts.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.title_label.set_text(&title);
|
this.title_label.set_text(&title);
|
||||||
this.subtitle_label.set_text(&item.track_set.recording.get_performers());
|
this.subtitle_label.set_text(&track.recording.get_performers());
|
||||||
this.position_label.set_text("0:00");
|
this.position_label.set_text("0:00");
|
||||||
|
|
||||||
this.current_item.set(current_item);
|
|
||||||
this.current_track.set(current_track);
|
this.current_track.set(current_track);
|
||||||
|
|
||||||
this.show_playlist();
|
this.show_playlist();
|
||||||
|
|
@ -205,29 +205,17 @@ impl Screen<(), ()> for PlayerScreen {
|
||||||
|
|
||||||
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||||
let widget = match this.items.borrow()[index] {
|
let widget = match this.items.borrow()[index] {
|
||||||
ListItem::Header(item_index) => {
|
ListItem::Track {index, first, playing} => {
|
||||||
let playlist_item = &this.playlist.borrow()[item_index];
|
let track = &this.playlist.borrow()[index];
|
||||||
let recording = &playlist_item.track_set.recording;
|
|
||||||
|
|
||||||
let row = libadwaita::ActionRow::new();
|
|
||||||
row.set_activatable(false);
|
|
||||||
row.set_selectable(false);
|
|
||||||
row.set_title(Some(&recording.work.get_title()));
|
|
||||||
row.set_subtitle(Some(&recording.get_performers()));
|
|
||||||
|
|
||||||
row.upcast()
|
|
||||||
}
|
|
||||||
ListItem::Track(item_index, track_index, playing) => {
|
|
||||||
let playlist_item = &this.playlist.borrow()[item_index];
|
|
||||||
let index = playlist_item.indices[track_index];
|
|
||||||
let track = &playlist_item.track_set.tracks[index];
|
|
||||||
|
|
||||||
let mut parts = Vec::<String>::new();
|
let mut parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
parts.push(playlist_item.track_set.recording.work.parts[*part].title.clone());
|
parts.push(track.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = if parts.is_empty() {
|
let title = if first {
|
||||||
|
track.recording.work.get_title()
|
||||||
|
} else if parts.is_empty() {
|
||||||
gettext("Unknown")
|
gettext("Unknown")
|
||||||
} else {
|
} else {
|
||||||
parts.join(", ")
|
parts.join(", ")
|
||||||
|
|
@ -238,8 +226,18 @@ impl Screen<(), ()> for PlayerScreen {
|
||||||
row.set_activatable(true);
|
row.set_activatable(true);
|
||||||
row.set_title(Some(&title));
|
row.set_title(Some(&title));
|
||||||
|
|
||||||
|
if first {
|
||||||
|
let subtitle = if !parts.is_empty() {
|
||||||
|
format!("{}\n{}", track.recording.get_performers(), parts.join(", "))
|
||||||
|
} else {
|
||||||
|
track.recording.get_performers()
|
||||||
|
};
|
||||||
|
|
||||||
|
row.set_subtitle(Some(&subtitle));
|
||||||
|
}
|
||||||
|
|
||||||
row.connect_activated(clone!(@weak this => move |_| {
|
row.connect_activated(clone!(@weak this => move |_| {
|
||||||
this.handle.backend.pl().set_track(item_index, track_index).unwrap();
|
this.handle.backend.pl().set_track(index).unwrap();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let icon = if playing {
|
let icon = if playing {
|
||||||
|
|
@ -272,25 +270,35 @@ impl PlayerScreen {
|
||||||
/// Update the user interface according to the playlist.
|
/// Update the user interface according to the playlist.
|
||||||
fn show_playlist(&self) {
|
fn show_playlist(&self) {
|
||||||
let playlist = self.playlist.borrow();
|
let playlist = self.playlist.borrow();
|
||||||
let current_item = self.current_item.get();
|
|
||||||
let current_track = self.current_track.get();
|
let current_track = self.current_track.get();
|
||||||
|
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
|
||||||
for (item_index, playlist_item) in playlist.iter().enumerate() {
|
let mut last_recording_id = "";
|
||||||
|
|
||||||
|
for (index, track) in playlist.iter().enumerate() {
|
||||||
|
let first_track = if track.recording.id != last_recording_id {
|
||||||
|
last_recording_id = &track.recording.id;
|
||||||
|
|
||||||
if !first {
|
if !first {
|
||||||
items.push(ListItem::Separator);
|
items.push(ListItem::Separator);
|
||||||
} else {
|
} else {
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(ListItem::Header(item_index));
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
for (index, _) in playlist_item.indices.iter().enumerate() {
|
let item = ListItem::Track {
|
||||||
let playing = current_item == item_index && current_track == index;
|
index,
|
||||||
items.push(ListItem::Track(item_index, index, playing));
|
first: first_track,
|
||||||
}
|
playing: index == current_track,
|
||||||
|
};
|
||||||
|
|
||||||
|
items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
let length = items.len();
|
let length = items.len();
|
||||||
|
|
|
||||||
|
|
@ -6,29 +6,17 @@ use gettextrs::gettext;
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use libadwaita::prelude::*;
|
use libadwaita::prelude::*;
|
||||||
use musicus_backend::PlaylistItem;
|
use musicus_backend::db::{Recording, Track};
|
||||||
use musicus_backend::db::{Recording, TrackSet};
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// Representation of one entry within the track list.
|
|
||||||
enum ListItem {
|
|
||||||
/// A track row. This hold an index to the track set and an index to the
|
|
||||||
/// track within the track set.
|
|
||||||
Track(usize, usize),
|
|
||||||
|
|
||||||
/// A separator intended for use between track sets.
|
|
||||||
Separator,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A screen for showing a recording.
|
/// A screen for showing a recording.
|
||||||
pub struct RecordingScreen {
|
pub struct RecordingScreen {
|
||||||
handle: NavigationHandle<()>,
|
handle: NavigationHandle<()>,
|
||||||
recording: Recording,
|
recording: Recording,
|
||||||
widget: widgets::Screen,
|
widget: widgets::Screen,
|
||||||
list: Rc<List>,
|
list: Rc<List>,
|
||||||
track_sets: RefCell<Vec<TrackSet>>,
|
tracks: RefCell<Vec<Track>>,
|
||||||
items: RefCell<Vec<ListItem>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screen<Recording, ()> for RecordingScreen {
|
impl Screen<Recording, ()> for RecordingScreen {
|
||||||
|
|
@ -48,22 +36,12 @@ impl Screen<Recording, ()> for RecordingScreen {
|
||||||
recording,
|
recording,
|
||||||
widget,
|
widget,
|
||||||
list,
|
list,
|
||||||
track_sets: RefCell::new(Vec::new()),
|
tracks: RefCell::new(Vec::new()),
|
||||||
items: RefCell::new(Vec::new()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
section.add_action("media-playback-start-symbolic", clone!(@weak this => move || {
|
section.add_action("media-playback-start-symbolic", clone!(@weak this => move || {
|
||||||
if let Some(player) = this.handle.backend.get_player() {
|
for track in &*this.tracks.borrow() {
|
||||||
if let Some(track_set) = this.track_sets.borrow().get(0).cloned() {
|
this.handle.backend.pl().add_item(track.clone()).unwrap();
|
||||||
let indices = (0..track_set.tracks.len()).collect();
|
|
||||||
|
|
||||||
let playlist_item = PlaylistItem {
|
|
||||||
track_set,
|
|
||||||
indices,
|
|
||||||
};
|
|
||||||
|
|
||||||
player.add_item(playlist_item).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -86,10 +64,7 @@ impl Screen<Recording, ()> for RecordingScreen {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
|
||||||
let widget = match this.items.borrow()[index] {
|
let track = &this.tracks.borrow()[index];
|
||||||
ListItem::Track(track_set_index, track_index) => {
|
|
||||||
let track_set = &this.track_sets.borrow()[track_set_index];
|
|
||||||
let track = &track_set.tracks[track_index];
|
|
||||||
|
|
||||||
let mut title_parts = Vec::<String>::new();
|
let mut title_parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
|
|
@ -106,27 +81,19 @@ impl Screen<Recording, ()> for RecordingScreen {
|
||||||
row.set_title(Some(&title));
|
row.set_title(Some(&title));
|
||||||
|
|
||||||
row.upcast()
|
row.upcast()
|
||||||
}
|
|
||||||
ListItem::Separator => {
|
|
||||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
|
||||||
separator.upcast()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
widget
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Load the content asynchronously.
|
// Load the content asynchronously.
|
||||||
|
|
||||||
spawn!(@clone this, async move {
|
spawn!(@clone this, async move {
|
||||||
let track_sets = this.handle
|
let tracks = this.handle
|
||||||
.backend
|
.backend
|
||||||
.db()
|
.db()
|
||||||
.get_track_sets(&this.recording.id)
|
.get_tracks(&this.recording.id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
this.show_track_sets(track_sets);
|
this.show_tracks(tracks);
|
||||||
this.widget.ready();
|
this.widget.ready();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -135,26 +102,10 @@ impl Screen<Recording, ()> for RecordingScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordingScreen {
|
impl RecordingScreen {
|
||||||
/// Update the track sets variable as well as the user interface.
|
/// Update the tracks variable as well as the user interface.
|
||||||
fn show_track_sets(&self, track_sets: Vec<TrackSet>) {
|
fn show_tracks(&self, tracks: Vec<Track>) {
|
||||||
let mut first = true;
|
let length = tracks.len();
|
||||||
let mut items = Vec::new();
|
self.tracks.replace(tracks);
|
||||||
|
|
||||||
for (track_set_index, track_set) in track_sets.iter().enumerate() {
|
|
||||||
if !first {
|
|
||||||
items.push(ListItem::Separator);
|
|
||||||
} else {
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (track_index, _) in track_set.tracks.iter().enumerate() {
|
|
||||||
items.push(ListItem::Track(track_set_index, track_index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = items.len();
|
|
||||||
self.items.replace(items);
|
|
||||||
self.track_sets.replace(track_sets);
|
|
||||||
self.list.update(length);
|
self.list.update(length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk_macros::get_widget;
|
use gtk_macros::get_widget;
|
||||||
use musicus_backend::{Player, PlaylistItem};
|
use musicus_backend::Player;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ impl PlayerBar {
|
||||||
self.player.replace(player.clone());
|
self.player.replace(player.clone());
|
||||||
|
|
||||||
if let Some(player) = player {
|
if let Some(player) = player {
|
||||||
let playlist = Rc::new(RefCell::new(Vec::<PlaylistItem>::new()));
|
let playlist = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
||||||
player.add_playlist_cb(clone!(
|
player.add_playlist_cb(clone!(
|
||||||
@strong player,
|
@strong player,
|
||||||
|
|
@ -107,25 +107,24 @@ impl PlayerBar {
|
||||||
@strong self.title_label as title_label,
|
@strong self.title_label as title_label,
|
||||||
@strong self.subtitle_label as subtitle_label,
|
@strong self.subtitle_label as subtitle_label,
|
||||||
@strong self.position_label as position_label
|
@strong self.position_label as position_label
|
||||||
=> move |current_item, current_track| {
|
=> move |current_track| {
|
||||||
previous_button.set_sensitive(player.has_previous());
|
previous_button.set_sensitive(player.has_previous());
|
||||||
next_button.set_sensitive(player.has_next());
|
next_button.set_sensitive(player.has_next());
|
||||||
|
|
||||||
let item = &playlist.borrow()[current_item];
|
let track = &playlist.borrow()[current_track];
|
||||||
let track = &item.track_set.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.track_set.recording.work.parts[*part].title.clone());
|
parts.push(track.recording.work.parts[*part].title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = item.track_set.recording.work.get_title();
|
let mut title = track.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.track_set.recording.get_performers());
|
subtitle_label.set_text(&track.recording.get_performers());
|
||||||
position_label.set_text("0:00");
|
position_label.set_text("0:00");
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue