Merge pull request #191 from kveld9/feat/search-context-menu

added context menu to search panel songs
This commit is contained in:
Psychotoxical
2026-04-15 18:27:06 +02:00
committed by GitHub
4 changed files with 75 additions and 17 deletions
+14 -2
View File
@@ -91,16 +91,28 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
const newIds = songIds.filter((id) => !existingIds.has(id)); const newIds = songIds.filter((id) => !existingIds.has(id));
if (newIds.length > 0) { if (newIds.length > 0) {
await updatePlaylist(pl.id, [...songs.map((s) => s.id), ...newIds]); await updatePlaylist(pl.id, [...songs.map((s) => s.id), ...newIds]);
showToast(t('playlists.addSuccess', { count: newIds.length, playlist: pl.name }));
} else {
showToast(t('playlists.addAllSkipped', { count: songIds.length, playlist: pl.name }), 3000, 'info');
} }
touchPlaylist(pl.id); touchPlaylist(pl.id);
} catch {} } catch {
showToast(t('playlists.addError'), 3000, 'error');
}
setAdding(null); setAdding(null);
onDone(); onDone();
}; };
const handleCreate = async () => { const handleCreate = async () => {
const name = newName.trim() || t('playlists.unnamed'); const name = newName.trim() || t('playlists.unnamed');
await createPlaylist(name, songIds); try {
const pl = await createPlaylist(name, songIds);
if (pl?.id) {
showToast(t('playlists.createAndAddSuccess', { count: songIds.length, playlist: pl.name || name }));
}
} catch {
showToast(t('playlists.createError'), 3000, 'error');
}
onDone(); onDone();
}; };
+24 -4
View File
@@ -12,6 +12,7 @@ import ArtistRow from '../components/ArtistRow';
import CustomSelect from '../components/CustomSelect'; import CustomSelect from '../components/CustomSelect';
import { useDragDrop } from '../contexts/DragDropContext'; import { useDragDrop } from '../contexts/DragDropContext';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useShallow } from 'zustand/react/shallow';
type ResultType = 'all' | 'artists' | 'albums' | 'songs'; type ResultType = 'all' | 'artists' | 'albums' | 'songs';
@@ -35,8 +36,23 @@ export default function AdvancedSearch() {
const qFromUrl = params.get('q') ?? ''; const qFromUrl = params.get('q') ?? '';
const navigate = useNavigate(); const navigate = useNavigate();
const psyDrag = useDragDrop(); const psyDrag = useDragDrop();
const playTrack = usePlayerStore(s => s.playTrack); const total = results
const openContextMenu = usePlayerStore(s => s.openContextMenu); ? results.artists.length + results.albums.length + results.songs.length
: 0;
const { playTrack, openContextMenu } = usePlayerStore(
useShallow(s => ({
playTrack: s.playTrack,
openContextMenu: s.openContextMenu,
}))
);
const [contextMenuSongId, setContextMenuSongId] = useState<string | null>(null);
const contextMenuOpen = usePlayerStore(s => s.contextMenu.isOpen);
useEffect(() => {
if (!contextMenuOpen) setContextMenuSongId(null);
}, [contextMenuOpen]);
const [query, setQuery] = useState(params.get('q') ?? ''); const [query, setQuery] = useState(params.get('q') ?? '');
const [genre, setGenre] = useState(''); const [genre, setGenre] = useState('');
@@ -288,11 +304,15 @@ export default function AdvancedSearch() {
return ( return (
<div <div
key={song.id} key={song.id}
className="track-row" className={`track-row${contextMenuSongId === song.id ? ' context-active' : ''}`}
style={{ gridTemplateColumns: '60px minmax(150px, 1fr) minmax(80px, 1fr) minmax(80px, 1fr) 90px 65px' }} style={{ gridTemplateColumns: '60px minmax(150px, 1fr) minmax(80px, 1fr) minmax(80px, 1fr) 90px 65px' }}
onDoubleClick={() => playTrack(track, results.songs.map(songToTrack))} onDoubleClick={() => playTrack(track, results.songs.map(songToTrack))}
role="row" role="row"
onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, track, 'song'); }} onContextMenu={e => {
e.preventDefault();
setContextMenuSongId(song.id);
openContextMenu(e.clientX, e.clientY, track, 'song');
}}
onMouseDown={e => { onMouseDown={e => {
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
+13 -7
View File
@@ -897,6 +897,7 @@ export default function PlaylistDetail() {
savePlaylist(next); savePlaylist(next);
setSuggestions(prev => prev.filter(s => s.id !== song.id)); setSuggestions(prev => prev.filter(s => s.id !== song.id));
setSearchResults(prev => prev.filter(s => s.id !== song.id)); setSearchResults(prev => prev.filter(s => s.id !== song.id));
showToast(t('playlists.addSuccess', { count: 1, playlist: playlist?.name }));
}; };
// ── Rating / Star ───────────────────────────────────────────── // ── Rating / Star ─────────────────────────────────────────────
@@ -1328,13 +1329,18 @@ export default function PlaylistDetail() {
)} )}
{searchResults.map(song => { {searchResults.map(song => {
const isSelected = selectedSearchIds.has(song.id); const isSelected = selectedSearchIds.has(song.id);
return ( return (
<div <div
key={song.id} key={song.id}
className={`playlist-search-row${isSelected ? ' playlist-search-row--selected' : ''}`} className={`playlist-search-row${isSelected ? ' playlist-search-row--selected' : ''}${contextMenuSongId === song.id ? ' context-active' : ''}`}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => addSong(song)} onClick={() => addSong(song)}
> onContextMenu={e => {
e.preventDefault();
setContextMenuSongId(song.id);
openContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song');
}}
>
<input <input
type="checkbox" type="checkbox"
className="playlist-search-checkbox" className="playlist-search-checkbox"
+24 -4
View File
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
import { useDragDrop } from '../contexts/DragDropContext'; import { useDragDrop } from '../contexts/DragDropContext';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useThemeStore } from '../store/themeStore'; import { useThemeStore } from '../store/themeStore';
import { useShallow } from 'zustand/react/shallow';
function formatDuration(s: number) { function formatDuration(s: number) {
return `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`; return `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`;
@@ -20,11 +21,25 @@ export default function SearchResults() {
const query = params.get('q') ?? ''; const query = params.get('q') ?? '';
const [results, setResults] = useState<ISearchResults | null>(null); const [results, setResults] = useState<ISearchResults | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const playTrack = usePlayerStore(s => s.playTrack);
const currentTrack = usePlayerStore(s => s.currentTrack);
const psyDrag = useDragDrop();
const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion); const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion);
const showBitrate = useThemeStore(s => s.showBitrate); const showBitrate = useThemeStore(s => s.showBitrate);
const psyDrag = useDragDrop();
const { playTrack, enqueue, openContextMenu, currentTrack } = usePlayerStore(
useShallow(s => ({
playTrack: s.playTrack,
enqueue: s.enqueue,
openContextMenu: s.openContextMenu,
currentTrack: s.currentTrack,
}))
);
const [contextMenuSongId, setContextMenuSongId] = useState<string | null>(null);
const contextMenuOpen = usePlayerStore(s => s.contextMenu.isOpen);
useEffect(() => {
if (!contextMenuOpen) setContextMenuSongId(null);
}, [contextMenuOpen]);
useEffect(() => { useEffect(() => {
if (!query.trim()) { setResults(null); return; } if (!query.trim()) { setResults(null); return; }
@@ -84,9 +99,14 @@ export default function SearchResults() {
{results.songs.map(song => ( {results.songs.map(song => (
<div <div
key={song.id} key={song.id}
className={`track-row${currentTrack?.id === song.id ? ' active' : ''}`} className={`track-row${currentTrack?.id === song.id ? ' active' : ''}${contextMenuSongId === song.id ? ' context-active' : ''}`}
style={{ gridTemplateColumns: '60px minmax(150px, 1fr) minmax(80px, 1fr) minmax(80px, 1fr) 100px 65px' }} style={{ gridTemplateColumns: '60px minmax(150px, 1fr) minmax(80px, 1fr) minmax(80px, 1fr) 100px 65px' }}
onDoubleClick={() => playSong(song, results.songs)} onDoubleClick={() => playSong(song, results.songs)}
onContextMenu={e => {
e.preventDefault();
setContextMenuSongId(song.id);
openContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song');
}}
role="row" role="row"
onMouseDown={e => { onMouseDown={e => {
if (e.button !== 0) return; if (e.button !== 0) return;