mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Add playlist view
This commit is contained in:
parent
16d1408194
commit
7d21617e9a
14 changed files with 430 additions and 57 deletions
|
|
@ -34,4 +34,24 @@
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist > row {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlisttile .title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlisttile .subtitle {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlisttile .parttitle {
|
||||||
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +22,15 @@ template $MusicusPlaylistPage : Adw.Bin {
|
||||||
Adw.Clamp {
|
Adw.Clamp {
|
||||||
maximum-size: 1000;
|
maximum-size: 1000;
|
||||||
tightening-threshold: 600;
|
tightening-threshold: 600;
|
||||||
|
|
||||||
|
Gtk.ListView playlist {
|
||||||
|
styles ["playlist", "background"]
|
||||||
|
margin-top: 12;
|
||||||
|
margin-bottom: 12;
|
||||||
|
margin-start: 12;
|
||||||
|
margin-end: 12;
|
||||||
|
single-click-activate: true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
data/ui/playlist_tile.blp
Normal file
45
data/ui/playlist_tile.blp
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $MusicusPlaylistTile : Gtk.Box {
|
||||||
|
styles ["playlisttile"]
|
||||||
|
|
||||||
|
Adw.Bin {
|
||||||
|
width-request: 48;
|
||||||
|
|
||||||
|
Gtk.Image playing_icon {
|
||||||
|
visible: false;
|
||||||
|
icon-name: "media-playback-start-symbolic";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.Box {
|
||||||
|
margin-end: 12;
|
||||||
|
orientation: vertical;
|
||||||
|
|
||||||
|
Gtk.Label title_label {
|
||||||
|
styles ["title"]
|
||||||
|
visible: false;
|
||||||
|
margin-top: 24;
|
||||||
|
halign: start;
|
||||||
|
wrap: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.Label performances_label {
|
||||||
|
styles ["subtitle", "dim-label"]
|
||||||
|
visible: false;
|
||||||
|
halign: start;
|
||||||
|
wrap: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.Label part_title_label {
|
||||||
|
styles ["parttitle"]
|
||||||
|
margin-top: 12;
|
||||||
|
margin-bottom: 12;
|
||||||
|
visible: false;
|
||||||
|
margin-start: 24;
|
||||||
|
halign: start;
|
||||||
|
wrap: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,13 +16,6 @@ template $MusicusWindow : Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Gtk.StackPage {
|
|
||||||
name: "playlist";
|
|
||||||
child: $MusicusPlaylistPage {
|
|
||||||
close => $hide_playlist() swapped;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[bottom]
|
[bottom]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
data/ui/home_page.blp
|
data/ui/home_page.blp
|
||||||
data/ui/playlist_page.blp
|
data/ui/playlist_page.blp
|
||||||
|
data/ui/playlist_tile.blp
|
||||||
data/ui/recording_tile.blp
|
data/ui/recording_tile.blp
|
||||||
data/ui/search_entry.blp
|
data/ui/search_entry.blp
|
||||||
data/ui/search_tag.blp
|
data/ui/search_tag.blp
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
library::{Ensemble, LibraryQuery, MusicusLibrary, Person, Recording, Work},
|
library::{Ensemble, LibraryQuery, MusicusLibrary, Person, Recording, Track, Work},
|
||||||
player::MusicusPlayer,
|
player::MusicusPlayer,
|
||||||
|
playlist_item::PlaylistItem,
|
||||||
recording_tile::MusicusRecordingTile,
|
recording_tile::MusicusRecordingTile,
|
||||||
search_entry::MusicusSearchEntry,
|
search_entry::MusicusSearchEntry,
|
||||||
search_tag::Tag,
|
search_tag::Tag,
|
||||||
|
|
@ -158,7 +159,62 @@ impl MusicusHomePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play_recording(&self, recording: &Recording) {
|
fn play_recording(&self, recording: &Recording) {
|
||||||
log::info!("Play recording: {:?}", recording)
|
let tracks = self.library().tracks(recording);
|
||||||
|
|
||||||
|
if tracks.is_empty() {
|
||||||
|
log::warn!("Ignoring recording without tracks being added to the playlist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = format!(
|
||||||
|
"{}: {}",
|
||||||
|
recording.work.composer.name_fl(),
|
||||||
|
recording.work.title
|
||||||
|
);
|
||||||
|
|
||||||
|
let performances = self.library().performances(recording);
|
||||||
|
let performances = if performances.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(performances.join(", "))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
if tracks.len() == 1 {
|
||||||
|
items.push(PlaylistItem::new(
|
||||||
|
&title,
|
||||||
|
performances.as_ref().map(|x| x.as_str()),
|
||||||
|
None,
|
||||||
|
&tracks[0].path,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let work_parts = self.library().work_parts(&recording.work);
|
||||||
|
let mut tracks = tracks.into_iter();
|
||||||
|
let first_track = tracks.next().unwrap();
|
||||||
|
|
||||||
|
let track_title = |track: &Track| -> String {
|
||||||
|
track
|
||||||
|
.work_parts
|
||||||
|
.iter()
|
||||||
|
.map(|w| work_parts[*w].clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
};
|
||||||
|
|
||||||
|
items.push(PlaylistItem::new(
|
||||||
|
&title,
|
||||||
|
performances.as_ref().map(|x| x.as_str()),
|
||||||
|
Some(&track_title(&first_track)),
|
||||||
|
&first_track.path,
|
||||||
|
));
|
||||||
|
|
||||||
|
while let Some(track) = tracks.next() {
|
||||||
|
items.push(PlaylistItem::new_part(&track_title(&track), &track.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.player().append(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query(&self, query: &LibraryQuery) {
|
fn query(&self, query: &LibraryQuery) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
|
||||||
use rusqlite::{Connection, Row};
|
use rusqlite::{Connection, Row};
|
||||||
use std::{
|
use std::{
|
||||||
cell::OnceCell,
|
cell::OnceCell,
|
||||||
|
num::ParseIntError,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -257,7 +258,51 @@ impl MusicusLibrary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn performances(&self, recording: &Recording) -> Vec<Performance> {
|
pub fn work_parts(&self, work: &Work) -> Vec<String> {
|
||||||
|
self.con()
|
||||||
|
.prepare("SELECT * FROM work_parts WHERE work IS ?1 ORDER BY part_index")
|
||||||
|
.unwrap()
|
||||||
|
.query_map([&work.id], |row| row.get::<_, String>(3))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<rusqlite::Result<Vec<String>>>()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tracks(&self, recording: &Recording) -> Vec<Track> {
|
||||||
|
self.con()
|
||||||
|
.prepare("SELECT * FROM tracks WHERE recording IS ?1 ORDER BY \"index\"")
|
||||||
|
.unwrap()
|
||||||
|
.query_map([&recording.id], |row| {
|
||||||
|
Ok(Track {
|
||||||
|
work_parts: row
|
||||||
|
.get::<_, String>(4)?
|
||||||
|
.split(',')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| str::parse::<usize>(s))
|
||||||
|
.collect::<Result<Vec<usize>, ParseIntError>>()
|
||||||
|
.expect("work part IDs should be valid integers"),
|
||||||
|
path: PathBuf::from(self.folder()).join(row.get::<_, String>(6)?),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.collect::<rusqlite::Result<Vec<Track>>>()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_recording(&self, query: &LibraryQuery) -> Option<Recording> {
|
||||||
|
match query {
|
||||||
|
LibraryQuery { .. } => self
|
||||||
|
.con()
|
||||||
|
.prepare("SELECT * FROM recordings ORDER BY RANDOM() LIMIT 1")
|
||||||
|
.unwrap()
|
||||||
|
.query_map([], Recording::from_row)
|
||||||
|
.unwrap()
|
||||||
|
.next()
|
||||||
|
.map(|r| r.unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn performances(&self, recording: &Recording) -> Vec<String> {
|
||||||
let mut performances = self
|
let mut performances = self
|
||||||
.con()
|
.con()
|
||||||
.prepare("SELECT persons.id, persons.first_name, persons.last_name, instruments.id, instruments.name FROM performances INNER JOIN persons ON persons.id = performances.person LEFT JOIN instruments ON instruments.id = performances.role INNER JOIN recordings ON performances.recording = recordings.id WHERE recordings.id IS ?1")
|
.prepare("SELECT persons.id, persons.first_name, persons.last_name, instruments.id, instruments.name FROM performances INNER JOIN persons ON persons.id = performances.person LEFT JOIN instruments ON instruments.id = performances.role INNER JOIN recordings ON performances.recording = recordings.id WHERE recordings.id IS ?1")
|
||||||
|
|
@ -277,6 +322,24 @@ impl MusicusLibrary {
|
||||||
.unwrap());
|
.unwrap());
|
||||||
|
|
||||||
performances
|
performances
|
||||||
|
.into_iter()
|
||||||
|
.map(|performance| match performance {
|
||||||
|
Performance::Person(person, role) => {
|
||||||
|
let mut result = person.name_fl();
|
||||||
|
if let Some(role) = role {
|
||||||
|
result.push_str(&format!(" ({})", role.name));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
Performance::Ensemble(ensemble, role) => {
|
||||||
|
let mut result = ensemble.name;
|
||||||
|
if let Some(role) = role {
|
||||||
|
result.push_str(&format!(" ({})", role.name));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn con(&self) -> &Connection {
|
fn con(&self) -> &Connection {
|
||||||
|
|
@ -472,3 +535,9 @@ impl PartialEq for Role {
|
||||||
self.id == other.id
|
self.id == other.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Track {
|
||||||
|
pub work_parts: Vec<usize>,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ mod config;
|
||||||
mod home_page;
|
mod home_page;
|
||||||
mod library;
|
mod library;
|
||||||
mod player;
|
mod player;
|
||||||
|
mod playlist_item;
|
||||||
mod playlist_page;
|
mod playlist_page;
|
||||||
|
mod playlist_tile;
|
||||||
mod recording_tile;
|
mod recording_tile;
|
||||||
mod search_entry;
|
mod search_entry;
|
||||||
mod search_tag;
|
mod search_tag;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
|
use crate::playlist_item::PlaylistItem;
|
||||||
use std::cell::Cell;
|
use gtk::{gio, glib, glib::Properties, prelude::*, subclass::prelude::*};
|
||||||
|
use std::cell::{Cell, OnceCell};
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -11,6 +12,10 @@ mod imp {
|
||||||
pub active: Cell<bool>,
|
pub active: Cell<bool>,
|
||||||
#[property(get, set)]
|
#[property(get, set)]
|
||||||
pub playing: Cell<bool>,
|
pub playing: Cell<bool>,
|
||||||
|
#[property(get, construct_only)]
|
||||||
|
pub playlist: OnceCell<gio::ListStore>,
|
||||||
|
#[property(get, set)]
|
||||||
|
pub current_index: Cell<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
|
@ -29,19 +34,30 @@ glib::wrapper! {
|
||||||
|
|
||||||
impl MusicusPlayer {
|
impl MusicusPlayer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
glib::Object::new()
|
glib::Object::builder()
|
||||||
|
.property("active", false)
|
||||||
|
.property("playing", false)
|
||||||
|
.property("playlist", gio::ListStore::new::<PlaylistItem>())
|
||||||
|
.property("current-index", 0u32)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&self, tracks: Vec<PlaylistItem>) {
|
||||||
|
let playlist = self.playlist();
|
||||||
|
|
||||||
|
for track in tracks {
|
||||||
|
playlist.append(&track);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_active(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&self) {
|
pub fn play(&self) {
|
||||||
if !self.imp().active.get() {
|
self.set_playing(true)
|
||||||
self.set_property("active", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_property("playing", true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause(&self) {
|
pub fn pause(&self) {
|
||||||
self.set_property("playing", false);
|
self.set_playing(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
66
src/playlist_item.rs
Normal file
66
src/playlist_item.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
|
||||||
|
use std::{
|
||||||
|
cell::OnceCell,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Properties, Default)]
|
||||||
|
#[properties(wrapper_type = super::PlaylistItem)]
|
||||||
|
pub struct PlaylistItem {
|
||||||
|
#[property(get, construct_only)]
|
||||||
|
pub is_title: OnceCell<bool>,
|
||||||
|
|
||||||
|
#[property(get, construct_only, nullable)]
|
||||||
|
pub title: OnceCell<Option<String>>,
|
||||||
|
|
||||||
|
#[property(get, construct_only, nullable)]
|
||||||
|
pub performers: OnceCell<Option<String>>,
|
||||||
|
|
||||||
|
#[property(get, construct_only, nullable)]
|
||||||
|
pub part_title: OnceCell<Option<String>>,
|
||||||
|
|
||||||
|
#[property(get, construct_only)]
|
||||||
|
pub path: OnceCell<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for PlaylistItem {
|
||||||
|
const NAME: &'static str = "MusicusPlaylistItem";
|
||||||
|
type Type = super::PlaylistItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::derived_properties]
|
||||||
|
impl ObjectImpl for PlaylistItem {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct PlaylistItem(ObjectSubclass<imp::PlaylistItem>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaylistItem {
|
||||||
|
pub fn new(
|
||||||
|
title: &str,
|
||||||
|
performers: Option<&str>,
|
||||||
|
part_title: Option<&str>,
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
) -> Self {
|
||||||
|
glib::Object::builder()
|
||||||
|
.property("is-title", true)
|
||||||
|
.property("title", title)
|
||||||
|
.property("performers", performers)
|
||||||
|
.property("part-title", part_title)
|
||||||
|
.property("path", path.as_ref())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_part(part_title: &str, path: impl AsRef<Path>) -> Self {
|
||||||
|
glib::Object::builder()
|
||||||
|
.property("is-title", false)
|
||||||
|
.property("part-title", part_title)
|
||||||
|
.property("path", path.as_ref())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
|
use crate::{player::MusicusPlayer, playlist_tile::PlaylistTile};
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use gtk::{glib, glib::subclass::Signal, prelude::*};
|
use gtk::{glib, glib::subclass::Signal, glib::Properties, prelude::*};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
|
use crate::playlist_item::PlaylistItem;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
|
||||||
|
#[properties(wrapper_type = super::MusicusPlayer)]
|
||||||
#[template(file = "data/ui/playlist_page.blp")]
|
#[template(file = "data/ui/playlist_page.blp")]
|
||||||
pub struct MusicusPlaylistPage {}
|
pub struct MusicusPlaylistPage {
|
||||||
|
#[property(get, construct_only)]
|
||||||
|
pub player: OnceCell<MusicusPlayer>,
|
||||||
|
|
||||||
|
#[template_child]
|
||||||
|
pub playlist: TemplateChild<gtk::ListView>,
|
||||||
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for MusicusPlaylistPage {
|
impl ObjectSubclass for MusicusPlaylistPage {
|
||||||
|
|
@ -25,6 +36,7 @@ mod imp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[glib::derived_properties]
|
||||||
impl ObjectImpl for MusicusPlaylistPage {
|
impl ObjectImpl for MusicusPlaylistPage {
|
||||||
fn signals() -> &'static [Signal] {
|
fn signals() -> &'static [Signal] {
|
||||||
static SIGNALS: Lazy<Vec<Signal>> =
|
static SIGNALS: Lazy<Vec<Signal>> =
|
||||||
|
|
@ -32,6 +44,30 @@ mod imp {
|
||||||
|
|
||||||
SIGNALS.as_ref()
|
SIGNALS.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn constructed(&self) {
|
||||||
|
self.parent_constructed();
|
||||||
|
|
||||||
|
self.playlist.set_model(Some(>k::NoSelection::new(Some(
|
||||||
|
self.player.get().unwrap().playlist(),
|
||||||
|
))));
|
||||||
|
|
||||||
|
let factory = gtk::SignalListItemFactory::new();
|
||||||
|
|
||||||
|
factory.connect_setup(|_, item| {
|
||||||
|
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||||
|
item.set_child(Some(&PlaylistTile::new()));
|
||||||
|
});
|
||||||
|
|
||||||
|
factory.connect_bind(|_, item| {
|
||||||
|
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||||
|
let tile = item.child().and_downcast::<PlaylistTile>().unwrap();
|
||||||
|
let playlist_item = item.item().and_downcast::<PlaylistItem>().unwrap();
|
||||||
|
tile.set_item(&playlist_item);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.playlist.set_factory(Some(&factory));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetImpl for MusicusPlaylistPage {}
|
impl WidgetImpl for MusicusPlaylistPage {}
|
||||||
|
|
@ -45,8 +81,16 @@ glib::wrapper! {
|
||||||
|
|
||||||
#[gtk::template_callbacks]
|
#[gtk::template_callbacks]
|
||||||
impl MusicusPlaylistPage {
|
impl MusicusPlaylistPage {
|
||||||
pub fn new() -> Self {
|
pub fn new(player: &MusicusPlayer) -> Self {
|
||||||
glib::Object::new()
|
glib::Object::builder().property("player", player).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect_close<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||||
|
self.connect_local("close", true, move |values| {
|
||||||
|
let obj = values[0].get::<Self>().unwrap();
|
||||||
|
f(&obj);
|
||||||
|
None
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
|
|
|
||||||
74
src/playlist_tile.rs
Normal file
74
src/playlist_tile.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
use crate::playlist_item::PlaylistItem;
|
||||||
|
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||||
|
#[template(file = "data/ui/playlist_tile.blp")]
|
||||||
|
pub struct PlaylistTile {
|
||||||
|
#[template_child]
|
||||||
|
pub playing_icon: TemplateChild<gtk::Image>,
|
||||||
|
#[template_child]
|
||||||
|
pub title_label: TemplateChild<gtk::Label>,
|
||||||
|
#[template_child]
|
||||||
|
pub performances_label: TemplateChild<gtk::Label>,
|
||||||
|
#[template_child]
|
||||||
|
pub part_title_label: TemplateChild<gtk::Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for PlaylistTile {
|
||||||
|
const NAME: &'static str = "MusicusPlaylistTile";
|
||||||
|
type Type = super::PlaylistTile;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) {
|
||||||
|
klass.bind_template();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||||
|
obj.init_template();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for PlaylistTile {}
|
||||||
|
impl WidgetImpl for PlaylistTile {}
|
||||||
|
impl BoxImpl for PlaylistTile {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct PlaylistTile(ObjectSubclass<imp::PlaylistTile>)
|
||||||
|
@extends gtk::Widget, gtk::FlowBoxChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaylistTile {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
glib::Object::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_item(&self, item: &PlaylistItem) {
|
||||||
|
let imp = self.imp();
|
||||||
|
|
||||||
|
if let Some(title) = item.title() {
|
||||||
|
imp.title_label.set_label(&title);
|
||||||
|
imp.title_label.set_visible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(performances) = item.performers() {
|
||||||
|
imp.performances_label.set_label(&performances);
|
||||||
|
imp.performances_label.set_visible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(part_title) = item.part_title() {
|
||||||
|
imp.part_title_label.set_label(&part_title);
|
||||||
|
imp.part_title_label.set_visible(true);
|
||||||
|
} else {
|
||||||
|
imp.obj().set_margin_bottom(24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_playing(&self, playing: bool) {
|
||||||
|
self.imp().playing_icon.set_visible(playing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::library::{Performance, Recording};
|
use crate::library::Recording;
|
||||||
use gtk::{glib, subclass::prelude::*};
|
use gtk::{glib, subclass::prelude::*};
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
|
|
@ -44,37 +44,14 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicusRecordingTile {
|
impl MusicusRecordingTile {
|
||||||
pub fn new(recording: &Recording, performances: Vec<Performance>) -> Self {
|
pub fn new(recording: &Recording, performances: Vec<String>) -> Self {
|
||||||
let obj: Self = glib::Object::new();
|
let obj: Self = glib::Object::new();
|
||||||
let imp = obj.imp();
|
let imp = obj.imp();
|
||||||
|
|
||||||
imp.work_label.set_label(&recording.work.title);
|
imp.work_label.set_label(&recording.work.title);
|
||||||
imp.composer_label
|
imp.composer_label
|
||||||
.set_label(&recording.work.composer.name_fl());
|
.set_label(&recording.work.composer.name_fl());
|
||||||
|
imp.performances_label.set_label(&performances.join(", "));
|
||||||
imp.performances_label.set_label(
|
|
||||||
&performances
|
|
||||||
.into_iter()
|
|
||||||
.map(|performance| match performance {
|
|
||||||
Performance::Person(person, role) => {
|
|
||||||
let mut result = person.name_fl();
|
|
||||||
if let Some(role) = role {
|
|
||||||
result.push_str(&format!(" ({})", role.name));
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
Performance::Ensemble(ensemble, role) => {
|
|
||||||
let mut result = ensemble.name;
|
|
||||||
if let Some(role) = role {
|
|
||||||
result.push_str(&format!(" ({})", role.name));
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", "),
|
|
||||||
);
|
|
||||||
|
|
||||||
imp.recording.set(recording.clone()).unwrap();
|
imp.recording.set(recording.clone()).unwrap();
|
||||||
|
|
||||||
obj
|
obj
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,6 @@ mod imp {
|
||||||
type ParentType = adw::ApplicationWindow;
|
type ParentType = adw::ApplicationWindow;
|
||||||
|
|
||||||
fn class_init(klass: &mut Self::Class) {
|
fn class_init(klass: &mut Self::Class) {
|
||||||
MusicusHomePage::static_type();
|
|
||||||
MusicusPlaylistPage::static_type();
|
|
||||||
MusicusWelcomePage::static_type();
|
MusicusWelcomePage::static_type();
|
||||||
klass.bind_template();
|
klass.bind_template();
|
||||||
klass.bind_template_instance_callbacks();
|
klass.bind_template_instance_callbacks();
|
||||||
|
|
@ -73,6 +71,14 @@ mod imp {
|
||||||
player.play();
|
player.play();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let playlist_page = MusicusPlaylistPage::new(&self.player);
|
||||||
|
let playlist_button = self.playlist_button.get();
|
||||||
|
playlist_page.connect_close(move |_| {
|
||||||
|
playlist_button.set_active(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.stack.add_named(&playlist_page, Some("playlist"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,9 +148,4 @@ impl MusicusWindow {
|
||||||
"navigation"
|
"navigation"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn hide_playlist(&self, _: &MusicusPlaylistPage) {
|
|
||||||
self.imp().playlist_button.set_active(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue