added "Add to Playlist" button to context menu

This commit is contained in:
MrDulfin 2025-05-27 06:55:17 -04:00
parent 99bf0b6855
commit bc79718dd5
8 changed files with 118 additions and 38 deletions

View file

@ -109,14 +109,12 @@ pub enum LibraryCommand {
AllSongs, AllSongs,
GetLibrary, GetLibrary,
ExternalPlaylist(Uuid), ExternalPlaylist(Uuid),
PlaylistSong{ PlaylistSong { list_uuid: Uuid, item_uuid: Uuid },
list_uuid: Uuid,
item_uuid: Uuid
},
Playlist(Uuid), Playlist(Uuid),
ImportM3UPlayList(PathBuf), ImportM3UPlayList(PathBuf),
Save, Save,
Playlists, Playlists,
PlaylistAddSong { playlist: Uuid, song: Uuid },
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -74,6 +74,15 @@ impl ControllerHandle {
Ok((uuid, name)) Ok((uuid, name))
} }
pub async fn playlist_add_song(&self, playlist: Uuid, song: Uuid) {
let (command, tx) =
LibraryCommandInput::command(LibraryCommand::PlaylistAddSong { playlist, song });
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,

View file

@ -89,15 +89,27 @@ impl Controller {
.await .await
.unwrap(); .unwrap();
} }
LibraryCommand::PlaylistSong { list_uuid, item_uuid } => { LibraryCommand::PlaylistSong {
let playlist= library.playlists.query_uuid(&list_uuid).unwrap(); list_uuid,
let Some((uuid, index)) = playlist.query_uuid(&item_uuid) else { todo!() }; item_uuid,
let Some((song, _)) = library.query_uuid(uuid) else { todo!() }; } => {
let playlist = library.playlists.query_uuid(&list_uuid).unwrap();
let Some((uuid, index)) = playlist.query_uuid(&item_uuid) else {
todo!()
};
let Some((song, _)) = library.query_uuid(uuid) else {
todo!()
};
res_rx res_rx
.send(LibraryResponse::PlaylistSong(song.clone(), index)) .send(LibraryResponse::PlaylistSong(song.clone(), index))
.await .await
.unwrap(); .unwrap();
} }
LibraryCommand::PlaylistAddSong { playlist, song } => {
let playlist = library.query_playlist_uuid_mut(&playlist).unwrap();
playlist.add_track(song);
res_rx.send(LibraryResponse::Ok).await.unwrap();
}
_ => { _ => {
todo!() todo!()
} }

View file

@ -1232,6 +1232,10 @@ impl MusicLibrary {
self.playlists.query_uuid(uuid) self.playlists.query_uuid(uuid)
} }
pub fn query_playlist_uuid_mut(&mut self, uuid: &Uuid) -> Option<&mut Playlist> {
self.playlists.query_uuid_mut(uuid)
}
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);
} }

View file

@ -55,6 +55,20 @@ impl PlaylistFolder {
None None
} }
pub fn query_uuid_mut(&mut self, uuid: &Uuid) -> Option<&mut Playlist> {
for item in &mut self.items {
match item {
PlaylistFolderItem::Folder(folder) => return folder.query_uuid_mut(uuid),
PlaylistFolderItem::List(playlist) => {
if &playlist.uuid == uuid {
return Some(playlist);
}
}
}
}
None
}
pub fn lists_recursive(&self) -> Vec<&Playlist> { pub fn lists_recursive(&self) -> Vec<&Playlist> {
let mut vec = vec![]; let mut vec = vec![];
for item in &self.items { for item in &self.items {

View file

@ -24,8 +24,8 @@ use uuid::Uuid;
use wrappers::{_Song, stop}; use wrappers::{_Song, stop};
use crate::wrappers::{ use crate::wrappers::{
get_library, get_playlist, get_playlists, get_queue, get_song, import_playlist, next, pause, add_song_to_playlist, get_library, get_playlist, get_playlists, get_queue, get_song,
play, prev, remove_from_queue, seek, set_volume, import_playlist, next, pause, play, 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};
@ -69,6 +69,7 @@ pub fn run() {
save_config, save_config,
close_window, close_window,
start_controller, start_controller,
add_song_to_playlist,
// test_menu, // test_menu,
]) ])
.manage(tempfile::TempDir::new().unwrap()) .manage(tempfile::TempDir::new().unwrap())

View file

@ -1,4 +1,4 @@
use std::{collections::BTreeMap, path::PathBuf}; use std::{collections::BTreeMap, path::PathBuf, thread::spawn};
use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
use crossbeam::channel::Sender; use crossbeam::channel::Sender;
@ -11,6 +11,7 @@ use dmp_core::{
}; };
use itertools::Itertools; use itertools::Itertools;
use serde::Serialize; use serde::Serialize;
use tauri::{AppHandle, Emitter, State, Wry}; use tauri::{AppHandle, Emitter, State, Wry};
use uuid::Uuid; use uuid::Uuid;
@ -217,14 +218,18 @@ pub async fn get_playlists(
ctrl_handle: State<'_, ControllerHandle>, ctrl_handle: State<'_, ControllerHandle>,
) -> Result<(), String> { ) -> Result<(), String> {
let lists = ctrl_handle.playlist_get_all().await; let lists = ctrl_handle.playlist_get_all().await;
app.emit( spawn(move || {
"playlists_gotten", futures::executor::block_on(async {
lists app.emit(
.into_iter() "playlists_gotten",
.map(|(uuid, name)| PlaylistPayload { uuid, name }) lists
.collect_vec(), .into_iter()
) .map(|(uuid, name)| PlaylistPayload { uuid, name })
.unwrap(); .collect_vec(),
)
.unwrap();
})
});
Ok(()) Ok(())
} }
@ -272,3 +277,12 @@ pub async fn get_song(
pub async fn seek(ctrl_handle: State<'_, ControllerHandle>, time: i64) -> Result<(), String> { pub async fn seek(ctrl_handle: State<'_, ControllerHandle>, time: i64) -> Result<(), String> {
ctrl_handle.seek(time).await.map_err(|e| e.to_string()) ctrl_handle.seek(time).await.map_err(|e| e.to_string())
} }
#[tauri::command]
pub async fn add_song_to_playlist(
ctrl_handle: State<'_, ControllerHandle>,
song: Uuid,
playlist: Uuid,
) -> Result<(), String> {
Ok(ctrl_handle.playlist_add_song(playlist, song).await)
}

View file

@ -1,4 +1,4 @@
import React, { createRef, ReactEventHandler, useEffect, useRef, useState } from "react"; import React, { createRef, MutableRefObject, ReactEventHandler, useEffect, useRef, useState } from "react";
import { convertFileSrc, invoke } from "@tauri-apps/api/core"; import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import "./App.css"; import "./App.css";
import { Config, playbackInfo } from "./types"; import { Config, playbackInfo } from "./types";
@ -7,7 +7,7 @@ import { Config, playbackInfo } from "./types";
// import { fetch } from "@tauri-apps/plugin-http"; // import { fetch } from "@tauri-apps/plugin-http";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { getCurrentWindow, LogicalPosition } from "@tauri-apps/api/window"; import { getCurrentWindow, LogicalPosition } from "@tauri-apps/api/window";
import { Menu } from "@tauri-apps/api/menu"; import { Menu, Submenu, SubmenuOptions } from "@tauri-apps/api/menu";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
const appWindow = getCurrentWebviewWindow(); const appWindow = getCurrentWebviewWindow();
@ -18,6 +18,7 @@ function App() {
const [playing, setPlaying] = useState(false); const [playing, setPlaying] = useState(false);
const [playlists, setPlaylists] = useState<JSX.Element[]>([]); const [playlists, setPlaylists] = useState<JSX.Element[]>([]);
const [viewName, setViewName] = useState("Library"); const [viewName, setViewName] = useState("Library");
const playlistsInfo= useRef<PlaylistInfo[]>([]);
const [nowPlaying, setNowPlaying] = useState<JSX.Element>( const [nowPlaying, setNowPlaying] = useState<JSX.Element>(
<NowPlaying <NowPlaying
@ -83,8 +84,8 @@ function App() {
<main> <main>
<div className="container"> <div className="container">
<div className="leftSide"> <div className="leftSide">
<PlaylistHead playlists={ playlists } setPlaylists={ setPlaylists } setViewName={ setViewName } setLibrary={ library[1] } /> <PlaylistHead playlists={ playlists } setPlaylists={ setPlaylists } setViewName={ setViewName } setLibrary={ library[1] } playlistsInfo={ playlistsInfo }/>
<MainView lib_ref={ library } viewName={ viewName } /> <MainView lib_ref={ library } viewName={ viewName } playlistsInfo={ playlistsInfo } />
</div> </div>
<div className="rightSide"> <div className="rightSide">
{ nowPlaying } { nowPlaying }
@ -100,22 +101,32 @@ function App() {
export default App; export default App;
interface PlaylistInfo {
uuid: string,
name: string,
}
interface PlaylistHeadProps { interface PlaylistHeadProps {
playlists: JSX.Element[] playlists: JSX.Element[]
setPlaylists: React.Dispatch<React.SetStateAction<JSX.Element[]>>, setPlaylists: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
setViewName: React.Dispatch<React.SetStateAction<string>>, setViewName: React.Dispatch<React.SetStateAction<string>>,
setLibrary: React.Dispatch<React.SetStateAction<JSX.Element[]>>, setLibrary: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
playlistsInfo: MutableRefObject<PlaylistInfo[]>,
} }
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: PlaylistHeadProps) { function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playlistsInfo }: PlaylistHeadProps) {
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => { const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => {
// console.log(event); // console.log(event);
let res = _res.payload; let res = _res.payload as PlaylistInfo[];
playlistsInfo.current = [...res];
console.log(playlistsInfo, res);
setPlaylists([ setPlaylists([
...res.map( (item) => { ...res.map( (item) => {
return ( return (
<button onClick={ () => { <button onClick={ () => {
invoke('get_playlist', { uuid: item.uuid }).then((list) => { invoke('get_playlist', { uuid: item.uuid }).then((list) => {
@ -130,6 +141,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
playlists={ playlistsInfo }
/> />
) )
})]) })])
@ -163,6 +175,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
playlists={ playlistsInfo }
/> />
) )
})]) })])
@ -190,6 +203,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
playlists={ playlistsInfo }
/> />
) )
})]) })])
@ -204,10 +218,11 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
interface MainViewProps { interface MainViewProps {
lib_ref: [JSX.Element[], React.Dispatch<React.SetStateAction<JSX.Element[]>>], lib_ref: [JSX.Element[], React.Dispatch<React.SetStateAction<JSX.Element[]>>],
viewName: string viewName: string,
playlistsInfo: MutableRefObject<PlaylistInfo[]>,
} }
function MainView({ lib_ref, viewName }: MainViewProps) { function MainView({ lib_ref, viewName, playlistsInfo }: MainViewProps) {
const [library, setLibrary] = lib_ref; const [library, setLibrary] = lib_ref;
useEffect(() => { useEffect(() => {
@ -225,6 +240,7 @@ function MainView({ lib_ref, viewName }: MainViewProps) {
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
playlists={ playlistsInfo }
/> />
) )
})]) })])
@ -255,7 +271,8 @@ interface SongProps {
last_played?: string, last_played?: string,
date_added?: string, date_added?: string,
date_modified?: string, date_modified?: string,
tags: any tags: any,
playlists: MutableRefObject<PlaylistInfo[]>
} }
function Song(props: SongProps) { function Song(props: SongProps) {
@ -264,15 +281,27 @@ function Song(props: SongProps) {
invoke('add_song_to_queue', { uuid: props.uuid, location: props.playerLocation }).then(() => {}); invoke('add_song_to_queue', { uuid: props.uuid, location: props.playerLocation }).then(() => {});
} }
const songMenuPromise = Menu.new({
items: [
{ id: "add_song_to_queue" + props.uuid, text: "Add to Queue", action: add_to_queue_test}
]
})
async function clickHandler(event: React.MouseEvent) { async function clickHandler(event: React.MouseEvent) {
event.preventDefault(); event.preventDefault();
const menu = await songMenuPromise; console.log(props.playlists);
const _ = await invoke('get_playlists');
const menu = await Menu.new({
items: [
{ id: "add_song_to_queue" + props.uuid, text: "Add to Queue", action: add_to_queue_test },
await Submenu.new(
{
text: "Add to Playlist...",
items: [...props.playlists.current.map((list) => {
const addToPlaylist = () => {
invoke('add_song_to_playlist', { playlist: list.uuid, song: props.uuid }).then(() => {});
}
return { id: "add_song_to_playlists" + props.uuid + list.uuid, text: list.name, action: addToPlaylist }
})]
} as SubmenuOptions
)
]
})
;
const pos = new LogicalPosition(event.clientX, event.clientY); const pos = new LogicalPosition(event.clientX, event.clientY);
menu.popup(pos); menu.popup(pos);
} }
@ -419,11 +448,10 @@ interface QueueSongProps {
function QueueSong({ song, location, index }: QueueSongProps) { function QueueSong({ song, location, index }: QueueSongProps) {
// console.log(song.tags); // console.log(song.tags);
let removeFromQueue = () => { const removeFromQueue = () => {
invoke('remove_from_queue', { index: index }).then(() => {}) invoke('remove_from_queue', { index: index }).then(() => {})
} }
const playNow = () => {
let playNow = () => {
invoke('play_now', { uuid: song.uuid, location: location }).then(() => {}) invoke('play_now', { uuid: song.uuid, location: location }).then(() => {})
} }