From 2b533cf1dbd99d54d961b0ea8075dd7e6e46c739 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 5 Mar 2025 00:10:23 -0800 Subject: [PATCH] Adds ability to rename favorite --- backend/src/FavoritesStore.ts | 18 ++++ backend/src/MediaPlayer.ts | 8 +- backend/src/server.ts | 16 +++ frontend/src/api/player.tsx | 10 ++ frontend/src/components/App.tsx | 58 +++++++---- .../src/components/RenameFavoriteModal.tsx | 97 +++++++++++++++++++ 6 files changed, 187 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/RenameFavoriteModal.tsx diff --git a/backend/src/FavoritesStore.ts b/backend/src/FavoritesStore.ts index c075ca2..19e1766 100644 --- a/backend/src/FavoritesStore.ts +++ b/backend/src/FavoritesStore.ts @@ -106,6 +106,24 @@ export class FavoritesStore { await this.saveFavorites(); } + async updateFavoriteTitle(filename: string, title: string): Promise { + console.log(`Updating title for favorite ${filename} to "${title}"`); + const index = this.favorites.findIndex(f => f.filename === filename); + if (index !== -1) { + // Create metadata object if it doesn't exist + if (!this.favorites[index].metadata) { + this.favorites[index].metadata = {}; + } + + // Update the title in metadata + this.favorites[index].metadata!.title = title; + + await this.saveFavorites(); + } else { + throw new Error(`Favorite with filename ${filename} not found`); + } + } + async clearFavorites(): Promise { this.favorites = []; await this.saveFavorites(); diff --git a/backend/src/MediaPlayer.ts b/backend/src/MediaPlayer.ts index b6983ee..7ae682b 100644 --- a/backend/src/MediaPlayer.ts +++ b/backend/src/MediaPlayer.ts @@ -186,6 +186,10 @@ export class MediaPlayer { return this.favoritesStore.clearFavorites(); } + public async updateFavoriteTitle(filename: string, title: string) { + return this.favoritesStore.updateFavoriteTitle(filename, title); + } + private async loadFile(url: string, mode: string) { this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("loadfile", [url, mode])); @@ -257,7 +261,7 @@ export class MediaPlayer { } private handleEvent(event: string, data: any) { - console.log("Event [" + event + "]: " + JSON.stringify(data, null, 2)); + console.log("Event [" + event + "]: ", data); // Notify all subscribers this.eventSubscribers.forEach(subscriber => { @@ -294,4 +298,4 @@ export class MediaPlayer { } } } -} \ No newline at end of file +} diff --git a/backend/src/server.ts b/backend/src/server.ts index d18a854..707ba12 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -158,6 +158,22 @@ apiRouter.delete("/favorites", withErrorHandling(async (req, res) => { res.send(JSON.stringify({ success: true })); })); +apiRouter.put("/favorites/:filename/title", withErrorHandling(async (req, res) => { + const { filename } = req.params as { filename: string }; + const { title } = req.body as { title: string }; + + if (!title) { + res.status(400).send(JSON.stringify({ + success: false, + error: "Title is required" + })); + return; + } + + await mediaPlayer.updateFavoriteTitle(filename, title); + res.send(JSON.stringify({ success: true })); +})); + // Serve static files for React app (after building) app.use(express.static(path.join(__dirname, "../dist/frontend"))); diff --git a/frontend/src/api/player.tsx b/frontend/src/api/player.tsx index db3c5da..ad3c0a4 100644 --- a/frontend/src/api/player.tsx +++ b/frontend/src/api/player.tsx @@ -158,5 +158,15 @@ export const API = { async clearFavorites(): Promise { await fetch('/api/favorites', { method: 'DELETE' }); + }, + + async updateFavoriteTitle(filename: string, title: string): Promise { + await fetch(`/api/favorites/${encodeURIComponent(filename)}/title`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title }), + }); } }; diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index 7bd3df1..3a2b061 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -2,9 +2,10 @@ import React, { useState, useEffect, useCallback, ReactNode } from 'react'; import SongTable from './SongTable'; import NowPlaying from './NowPlaying'; import AddSongPanel from './AddSongPanel'; +import RenameFavoriteModal from './RenameFavoriteModal'; import { TabView, Tab } from './TabView'; import { API, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player'; -import { FaMusic, FaHeart, FaPlus } from 'react-icons/fa'; +import { FaMusic, FaHeart, FaPlus, FaEdit } from 'react-icons/fa'; import useWebSocket from 'react-use-websocket'; import classNames from 'classnames'; @@ -88,6 +89,8 @@ const App: React.FC = () => { const [playlist, setPlaylist] = useState([]); const [favorites, setFavorites] = useState([]); const [selectedTab, setSelectedTab] = useState(Tabs.Playlist); + const [isRenameModalOpen, setIsRenameModalOpen] = useState(false); + const [favoriteToRename, setFavoriteToRename] = useState(null); const fetchPlaylist = useCallback(async () => { const playlist = await API.getPlaylist(); @@ -256,22 +259,35 @@ const App: React.FC = () => { const favoritesAuxControlProvider = (song: PlaylistItem) => { const isInPlaylist = playlist.some(p => p.filename === song.filename); return ( - { - if (isInPlaylist) { - API.removeFromPlaylist(playlist.findIndex(p => p.filename === song.filename)); - } else { - API.addToPlaylist(song.filename); - } - }} - > - - +
+ { + setFavoriteToRename(song); + setIsRenameModalOpen(true); + }} + > + + + + { + if (isInPlaylist) { + API.removeFromPlaylist(playlist.findIndex(p => p.filename === song.filename)); + } else { + API.addToPlaylist(song.filename); + } + }} + > + + +
); }; @@ -312,9 +328,15 @@ const App: React.FC = () => { + + setIsRenameModalOpen(false)} + favorite={favoriteToRename} + /> ); }; -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/components/RenameFavoriteModal.tsx b/frontend/src/components/RenameFavoriteModal.tsx new file mode 100644 index 0000000..53d6e5a --- /dev/null +++ b/frontend/src/components/RenameFavoriteModal.tsx @@ -0,0 +1,97 @@ +import React, { useState, KeyboardEvent, useEffect } from 'react'; +import { FaTimes, FaCheck } from 'react-icons/fa'; +import { API, PlaylistItem, getDisplayTitle } from '../api/player'; + +interface RenameFavoriteModalProps { + isOpen: boolean; + onClose: () => void; + favorite: PlaylistItem | null; +} + +const RenameFavoriteModal: React.FC = ({ isOpen, onClose, favorite }) => { + const [title, setTitle] = useState(''); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + if (favorite) { + setTitle(getDisplayTitle(favorite)); + } + }, [favorite]); + + const handleSave = async () => { + if (!favorite || !title.trim()) return; + + setIsSaving(true); + + try { + await API.updateFavoriteTitle(favorite.filename, title); + onClose(); + } catch (error) { + console.error('Failed to rename favorite:', error); + } finally { + setIsSaving(false); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleSave(); + } else if (e.key === 'Escape') { + onClose(); + } + }; + + if (!isOpen || !favorite) return null; + + return ( +
+
+
+

Rename Favorite

+ + +
+ +
+ +
+ {favorite.filename} +
+ + + setTitle(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter new title..." + className="p-2 rounded-lg border-2 border-violet-500 w-full bg-black/20 text-white" + /> +
+ +
+ + + +
+
+
+ ); +}; + +export default RenameFavoriteModal;