mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
added context menu to search panel songs
This commit is contained in:
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -366,6 +366,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 ─────────────────────────────────────────────
|
||||||
@@ -759,13 +760,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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user