mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-06-22 22:52:59 -05:00
Added context menu for QueueSongs and Playlists
This commit is contained in:
parent
bc79718dd5
commit
158e80cc8d
10 changed files with 179 additions and 19 deletions
|
@ -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)]
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
45
src/App.tsx
45
src/App.tsx
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue