Added context menu for QueueSongs and Playlists

This commit is contained in:
MrDulfin 2025-06-01 22:02:08 -04:00
parent bc79718dd5
commit 158e80cc8d
10 changed files with 179 additions and 19 deletions

View file

@ -115,6 +115,7 @@ pub enum LibraryCommand {
Save, Save,
Playlists, Playlists,
PlaylistAddSong { playlist: Uuid, song: Uuid }, PlaylistAddSong { playlist: Uuid, song: Uuid },
DeletePlaylist(Uuid),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -132,7 +133,7 @@ pub enum LibraryResponse {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum QueueCommand { pub enum QueueCommand {
Append(QueueItem_, bool), Append(QueueItem<QueueSong, QueueAlbum>, bool),
Next, Next,
Prev, Prev,
GetIndex(usize), GetIndex(usize),
@ -140,6 +141,7 @@ pub enum QueueCommand {
Get, Get,
Clear, Clear,
Remove(usize), Remove(usize),
PlayNext(QueueItem<QueueSong, QueueAlbum>, bool),
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]

View file

@ -1,12 +1,13 @@
use std::path::PathBuf; use std::path::PathBuf;
use async_channel::{Receiver, Sender}; use async_channel::{Receiver, Sender};
use discord_presence::models::Command;
use uuid::Uuid; use uuid::Uuid;
use crate::music_storage::{ use crate::music_storage::{
library::Song, library::Song,
playlist::ExternalPlaylist, playlist::ExternalPlaylist,
queue::{QueueError, QueueItem}, queue::{QueueError, QueueItem, QueueItemType},
}; };
use super::{ use super::{
@ -83,6 +84,14 @@ impl ControllerHandle {
}; };
} }
pub async fn playlist_delete(&self, playlist: Uuid) {
let (command, tx) = LibraryCommandInput::command(LibraryCommand::DeletePlaylist(playlist));
self.lib_mail_rx.send(command).await.unwrap();
let LibraryResponse::Ok = tx.recv().await.unwrap() else {
unreachable!()
};
}
// The Queue Section // The Queue Section
pub async fn queue_append( pub async fn queue_append(
&self, &self,
@ -117,6 +126,27 @@ impl ControllerHandle {
queue queue
} }
pub async fn queue_play_next(
&self,
uuid: Uuid,
location: PlayerLocation,
) -> Result<(), QueueError> {
let (command, tx) = LibraryCommandInput::command(LibraryCommand::Song(uuid));
self.lib_mail_rx.send(command).await.unwrap();
let LibraryResponse::Song(song, _) = tx.recv().await.unwrap() else {
unimplemented!()
};
let (command, tx) = QueueCommandInput::command(QueueCommand::PlayNext(
QueueItem::from_item_type(QueueItemType::from_single(QueueSong { song, location })),
false,
));
self.queue_mail_rx.send(command).await.unwrap();
let QueueResponse::Empty(_) = tx.recv().await.unwrap() else {
unimplemented!()
};
Ok(())
}
// The Player Section // The Player Section
pub async fn play_now(&self, uuid: Uuid, location: PlayerLocation) -> Result<Song, QueueError> { pub async fn play_now(&self, uuid: Uuid, location: PlayerLocation) -> Result<Song, QueueError> {
let (command, tx) = PlayerCommandInput::command(PlayerCommand::PlayNow(uuid, location)); let (command, tx) = PlayerCommandInput::command(PlayerCommand::PlayNow(uuid, location));

View file

@ -6,7 +6,7 @@ use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterato
use crate::{ use crate::{
config::Config, config::Config,
music_storage::{ music_storage::{
library::MusicLibrary, library::{self, MusicLibrary},
playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem}, playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem},
}, },
}; };
@ -108,6 +108,23 @@ impl Controller {
LibraryCommand::PlaylistAddSong { playlist, song } => { LibraryCommand::PlaylistAddSong { playlist, song } => {
let playlist = library.query_playlist_uuid_mut(&playlist).unwrap(); let playlist = library.query_playlist_uuid_mut(&playlist).unwrap();
playlist.add_track(song); playlist.add_track(song);
library.save(config.read().path.clone()).unwrap();
res_rx.send(LibraryResponse::Ok).await.unwrap();
}
LibraryCommand::DeletePlaylist(uuid) => {
_ = library.playlists.delete_uuid(uuid);
let lib_uuid = library.uuid;
library
.save(
config
.read()
.libraries
.get_library(&lib_uuid)
.unwrap()
.path
.clone(),
)
.unwrap();
res_rx.send(LibraryResponse::Ok).await.unwrap(); res_rx.send(LibraryResponse::Ok).await.unwrap();
} }
_ => { _ => {

View file

@ -1,4 +1,7 @@
use crate::music_storage::queue::{Queue, QueueError, QueueItemType}; use crate::music_storage::{
library::Song,
queue::{Queue, QueueError, QueueItemType},
};
use super::{ use super::{
controller::{Controller, QueueCommand, QueueResponse}, controller::{Controller, QueueCommand, QueueResponse},
@ -69,6 +72,17 @@ impl Controller {
.await .await
.unwrap(); .unwrap();
} }
QueueCommand::PlayNext(item, by_human) => {
match item.item {
QueueItemType::Single(song) => {
queue.add_item_next(song);
}
QueueItemType::Multi(album) => {
unimplemented!()
}
};
res_rx.send(QueueResponse::Empty(Ok(()))).await.unwrap();
}
} }
} }
} }

View file

@ -18,6 +18,7 @@ use file_format::{FileFormat, Kind};
use lofty::file::{AudioFile as _, TaggedFileExt as _}; use lofty::file::{AudioFile as _, TaggedFileExt as _};
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::{ItemKey, ItemValue, TagType}; use lofty::tag::{ItemKey, ItemValue, TagType};
use rayon::iter::plumbing::Folder;
use rcue::parser::parse_from_file; use rcue::parser::parse_from_file;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -1239,6 +1240,25 @@ impl MusicLibrary {
pub fn push_playlist(&mut self, playlist: PlaylistFolderItem) { pub fn push_playlist(&mut self, playlist: PlaylistFolderItem) {
self.playlists.items.push(playlist); self.playlists.items.push(playlist);
} }
pub fn delete_playlist(&mut self, uuid: Uuid) -> Option<PlaylistFolderItem> {
let mut index = None;
for (i, item) in self.playlists.items.iter_mut().enumerate() {
match item {
PlaylistFolderItem::Folder(folder) => return folder.delete_uuid(uuid),
PlaylistFolderItem::List(list) => {
if list.uuid == uuid {
index = Some(i);
}
}
}
}
if let Some(i) = index {
Some(self.playlists.items.remove(i))
} else {
None
}
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -11,6 +11,7 @@ use std::time::Duration;
// use chrono::Duration; // use chrono::Duration;
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI}; use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
use chrono::format::Item;
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -79,6 +80,25 @@ impl PlaylistFolder {
} }
vec vec
} }
pub fn delete_uuid(&mut self, uuid: Uuid) -> Option<PlaylistFolderItem> {
let mut index = None;
for (i, item) in &mut self.items.iter_mut().enumerate() {
match item {
PlaylistFolderItem::Folder(folder) => return folder.delete_uuid(uuid),
PlaylistFolderItem::List(playlist) => {
if playlist.uuid == uuid {
index = Some(i);
}
}
}
}
if let Some(i) = index {
Some(self.items.remove(i))
} else {
None
}
}
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]

View file

@ -32,7 +32,7 @@ pub enum QueueItemType<
impl< impl<
T: Debug + Clone + PartialEq, // T: The Singular Item Type T: Debug + Clone + PartialEq, // T: The Singular Item Type
U: Debug + PartialEq + Clone + IntoIterator, // U: The Multi-Item Type. Needs to be tracked as multiple items U: Debug + PartialEq + Clone + IntoIterator, // U: The Multi-Item Type. Needs to be tracked as multiple items
> QueueItemType<T, U> > QueueItemType<T, U>
{ {
pub fn from_single(item: T) -> Self { pub fn from_single(item: T) -> Self {
QueueItemType::Single(item) QueueItemType::Single(item)
@ -219,7 +219,7 @@ impl<T: Debug + Clone + PartialEq, U: Debug + PartialEq + Clone + IntoIterator>
} }
/// Add multiple Items after the currently playing Item /// Add multiple Items after the currently playing Item
pub fn add_multi_next(&mut self, items: Vec<QueueItemType<T, U>>) { pub fn add_multiple_next(&mut self, items: Vec<QueueItemType<T, U>>) {
use QueueState::*; use QueueState::*;
let empty = self.items.is_empty(); let empty = self.items.is_empty();

View file

@ -19,13 +19,14 @@ use dmp_core::{
music_storage::library::{MusicLibrary, Song}, music_storage::library::{MusicLibrary, Song},
}; };
use parking_lot::RwLock; use parking_lot::RwLock;
use tauri::{http::Response, AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager, http::Response};
use uuid::Uuid; use uuid::Uuid;
use wrappers::{_Song, stop}; use wrappers::{_Song, stop};
use crate::wrappers::{ use crate::wrappers::{
add_song_to_playlist, get_library, get_playlist, get_playlists, get_queue, get_song, add_song_to_playlist, delete_playlist, get_library, get_playlist, get_playlists, get_queue,
import_playlist, next, pause, play, prev, remove_from_queue, seek, set_volume, get_song, import_playlist, next, pause, play, play_next_queue, prev, remove_from_queue, seek,
set_volume,
}; };
use commands::{add_song_to_queue, display_album_art, last_fm_init_auth, play_now}; use commands::{add_song_to_queue, display_album_art, last_fm_init_auth, play_now};
@ -70,6 +71,8 @@ pub fn run() {
close_window, close_window,
start_controller, start_controller,
add_song_to_playlist, add_song_to_playlist,
delete_playlist,
play_next_queue,
// test_menu, // test_menu,
]) ])
.manage(tempfile::TempDir::new().unwrap()) .manage(tempfile::TempDir::new().unwrap())

View file

@ -1,6 +1,6 @@
use std::{collections::BTreeMap, path::PathBuf, thread::spawn}; use std::{collections::BTreeMap, path::PathBuf, thread::spawn};
use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; use chrono::{DateTime, Utc, serde::ts_milliseconds_option};
use crossbeam::channel::Sender; use crossbeam::channel::Sender;
use dmp_core::{ use dmp_core::{
music_controller::controller::{ControllerHandle, PlayerLocation}, music_controller::controller::{ControllerHandle, PlayerLocation},
@ -286,3 +286,26 @@ pub async fn add_song_to_playlist(
) -> Result<(), String> { ) -> Result<(), String> {
Ok(ctrl_handle.playlist_add_song(playlist, song).await) Ok(ctrl_handle.playlist_add_song(playlist, song).await)
} }
#[tauri::command]
pub async fn delete_playlist(
ctrl_handle: State<'_, ControllerHandle>,
uuid: Uuid,
) -> Result<(), String> {
Ok(ctrl_handle.playlist_delete(uuid).await)
}
#[tauri::command]
pub async fn play_next_queue(
app: AppHandle<Wry>,
ctrl_handle: State<'_, ControllerHandle>,
uuid: Uuid,
location: PlayerLocation,
) -> Result<(), String> {
let res = ctrl_handle
.queue_play_next(uuid, location)
.await
.map_err(|e| e.to_string());
app.emit("queue_updated", ()).unwrap();
res
}

View file

@ -125,18 +125,32 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
console.log(playlistsInfo, res); console.log(playlistsInfo, res);
setPlaylists([ setPlaylists([
...res.map( (item) => { ...res.map( (list) => {
const deletePlaylist = () => {
invoke('delete_playlist', { uuid: list.uuid }).then(() => {});
invoke('get_playlists').then(() => {});
}
async function menuHandler(event: React.MouseEvent) {
event.preventDefault();
const menu = await Menu.new({
items: [
{ id: "delete_playlist" + list.uuid, text: "Delete Playlist", action: deletePlaylist }
]
});
menu.popup();
}
return ( return (
<button onClick={ () => { <button onClick={ () => {
invoke('get_playlist', { uuid: item.uuid }).then((list) => { invoke('get_playlist', { uuid: list.uuid }).then((list_songs) => {
setLibrary([...(list as any[]).map((song) => { setLibrary([...(list_songs as any[]).map((song) => {
// console.log(song); // console.log(song);
return ( return (
<Song <Song
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) } key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
location={ song.location } location={ song.location }
playerLocation={ {"Playlist" : item.uuid } } playerLocation={ {"Playlist" : list.uuid } }
uuid={ song.uuid } uuid={ song.uuid }
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
@ -146,8 +160,11 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
) )
})]) })])
}) })
setViewName( item.name ) setViewName( list.name )
} } key={ 'playlist_' + item.uuid }>{ item.name }</button>
} }
onContextMenu={ menuHandler }
key={ 'playlist_' + list.uuid }>{ list.name }</button>
) )
}) })
]) ])
@ -164,6 +181,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
invoke('get_playlist', { uuid: res.uuid }).then((list) => { invoke('get_playlist', { uuid: res.uuid }).then((list) => {
console.log((list as any[]).length); console.log((list as any[]).length);
setLibrary([...(list as any[]).map((song) => { setLibrary([...(list as any[]).map((song) => {
// console.log(song); // console.log(song);
return ( return (
@ -454,9 +472,22 @@ function QueueSong({ song, location, index }: QueueSongProps) {
const playNow = () => { const playNow = () => {
invoke('play_now', { uuid: song.uuid, location: location }).then(() => {}) invoke('play_now', { uuid: song.uuid, location: location }).then(() => {})
} }
const play_next = () => invoke('play_next_queue', { uuid: song.uuid, location }).then(() => {});
async function menuHandler(event: React.MouseEvent) {
event.preventDefault();
const menu = await Menu.new({
items: [
{ id: "play_next_" + song.uuid + index, text: "Play Next in Queue", action: play_next },
{ id: "remove_queue" + song.uuid + index, text: "Remove from Queue", action: removeFromQueue }
]
})
menu.popup();
}
return ( return (
<div className="queueSong unselectable" onAuxClickCapture={ removeFromQueue } onDoubleClickCapture={ playNow }> <div className="queueSong unselectable" onAuxClickCapture={ removeFromQueue } onDoubleClickCapture={ playNow } onContextMenu={ menuHandler }>
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/> <img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
<div className="queueSongTags"> <div className="queueSongTags">
<p className="queueSongTitle">{ song.tags.TrackTitle }</p> <p className="queueSongTitle">{ song.tags.TrackTitle }</p>