mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fixed some bugs
This commit is contained in:
@@ -44,6 +44,7 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [flipLeft, setFlipLeft] = useState(false);
|
||||
const [flipUp, setFlipUp] = useState(false);
|
||||
const storePlaylists = usePlaylistStore((s) => s.playlists);
|
||||
const recentIds = usePlaylistStore((s) => s.recentIds);
|
||||
const createPlaylist = usePlaylistStore((s) => s.createPlaylist);
|
||||
@@ -62,10 +63,12 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
|
||||
}, [storePlaylists, recentIds]);
|
||||
|
||||
// Flip submenu left if it would overflow the right edge of the viewport
|
||||
// Flip submenu up if it would overflow the bottom of the viewport
|
||||
useLayoutEffect(() => {
|
||||
if (subRef.current) {
|
||||
const rect = subRef.current.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth - 8) setFlipLeft(true);
|
||||
if (rect.bottom > window.innerHeight - 8) setFlipUp(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -97,8 +100,8 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
|
||||
const subStyle: React.CSSProperties = dropDown
|
||||
? { top: 'calc(100% + 4px)', left: 0, right: 'auto' }
|
||||
: flipLeft
|
||||
? { right: 'calc(100% + 4px)', left: 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto' };
|
||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||
|
||||
return (
|
||||
<div className="context-submenu" data-parent-trigger-id={triggerId ?? ''} ref={subRef} style={subStyle}>
|
||||
@@ -198,14 +201,18 @@ function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId }: { albumIds
|
||||
const { t } = useTranslation();
|
||||
const [resolvedIds, setResolvedIds] = useState<string[] | null>(null);
|
||||
const [totalAlbums, setTotalAlbums] = useState(0);
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTotalAlbums(albumIds.length);
|
||||
// Delay showing loading state to avoid flash for fast loads
|
||||
const loadingTimeout = setTimeout(() => setShowLoading(true), 300);
|
||||
(async () => {
|
||||
const albumSongs = await Promise.all(albumIds.map(id => getAlbum(id).then(r => r.songs).catch(() => [])));
|
||||
const allSongs = albumSongs.flat();
|
||||
setResolvedIds(allSongs.map(s => s.id));
|
||||
})().catch(() => setResolvedIds([]));
|
||||
return () => clearTimeout(loadingTimeout);
|
||||
}, [albumIds]);
|
||||
|
||||
const handleAddWithToast = async (pl: SubsonicPlaylist, songIds: string[]) => {
|
||||
@@ -292,22 +299,26 @@ function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId }: { albumIds
|
||||
const { t } = useTranslation();
|
||||
const subRef = useRef<HTMLDivElement>(null);
|
||||
const newNameRef = useRef<HTMLInputElement>(null);
|
||||
const [playlists, setPlaylists] = useState<SubsonicPlaylist[]>([]);
|
||||
const [adding, setAdding] = useState<string | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [flipLeft, setFlipLeft] = useState(false);
|
||||
const [flipUp, setFlipUp] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const storePlaylists = usePlaylistStore((s) => s.playlists);
|
||||
|
||||
useEffect(() => {
|
||||
getPlaylists().then((all) => {
|
||||
setPlaylists(all.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
// Sort playlists from store (no fetch needed, prevents flash)
|
||||
const playlists = useMemo(() => {
|
||||
return [...storePlaylists].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [storePlaylists]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (subRef.current) {
|
||||
const rect = subRef.current.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth - 8) setFlipLeft(true);
|
||||
if (rect.bottom > window.innerHeight - 8) setFlipUp(true);
|
||||
// Show after position is calculated to prevent flash
|
||||
setVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -342,11 +353,11 @@ function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId }: { albumIds
|
||||
};
|
||||
|
||||
const subStyle: React.CSSProperties = flipLeft
|
||||
? { right: 'calc(100% + 4px)', left: 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto' };
|
||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||
|
||||
return (
|
||||
<div className="context-submenu" ref={subRef} style={subStyle}>
|
||||
<div className="context-submenu" ref={subRef} style={{ ...subStyle, visibility: visible ? 'visible' : 'hidden' }}>
|
||||
{!creating ? (
|
||||
<div
|
||||
className="context-menu-item context-submenu-new"
|
||||
@@ -394,8 +405,12 @@ function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId }: { albumIds
|
||||
}
|
||||
|
||||
if (resolvedIds === null) {
|
||||
// Only show loading UI if it takes more than 300ms (avoid flash)
|
||||
if (!showLoading) {
|
||||
return <div className="context-submenu" style={{ minWidth: 190 }} />;
|
||||
}
|
||||
return (
|
||||
<div className="context-submenu" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '0.75rem', gap: '0.5rem' }}>
|
||||
<div className="context-submenu" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '0.75rem', gap: '0.5rem', minWidth: 190 }}>
|
||||
<div className="spinner" style={{ width: 16, height: 16 }} />
|
||||
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
{t('playlists.loadingAlbums', { count: totalAlbums })}
|
||||
@@ -412,9 +427,12 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist
|
||||
const { t } = useTranslation();
|
||||
const [resolvedIds, setResolvedIds] = useState<string[] | null>(null);
|
||||
const [totalArtists, setTotalArtists] = useState(0);
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTotalArtists(artistIds.length);
|
||||
// Delay showing loading state to avoid flash for fast loads
|
||||
const loadingTimeout = setTimeout(() => setShowLoading(true), 300);
|
||||
(async () => {
|
||||
const allSongs: string[] = [];
|
||||
for (const artistId of artistIds) {
|
||||
@@ -428,6 +446,7 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist
|
||||
}
|
||||
setResolvedIds(allSongs);
|
||||
})().catch(() => setResolvedIds([]));
|
||||
return () => clearTimeout(loadingTimeout);
|
||||
}, [artistIds]);
|
||||
|
||||
const handleAddWithToast = async (pl: SubsonicPlaylist, songIds: string[]) => {
|
||||
@@ -495,6 +514,7 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [flipLeft, setFlipLeft] = useState(false);
|
||||
const [flipUp, setFlipUp] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getPlaylists().then((all) => {
|
||||
@@ -506,6 +526,7 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist
|
||||
if (subRef.current) {
|
||||
const rect = subRef.current.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth - 8) setFlipLeft(true);
|
||||
if (rect.bottom > window.innerHeight - 8) setFlipUp(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -540,8 +561,8 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist
|
||||
};
|
||||
|
||||
const subStyle: React.CSSProperties = flipLeft
|
||||
? { right: 'calc(100% + 4px)', left: 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto' };
|
||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||
|
||||
return (
|
||||
<div className="context-submenu" ref={subRef} style={subStyle}>
|
||||
@@ -592,8 +613,12 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist
|
||||
}
|
||||
|
||||
if (resolvedIds === null) {
|
||||
// Only show loading UI if it takes more than 300ms (avoid flash)
|
||||
if (!showLoading) {
|
||||
return <div className="context-submenu" style={{ minWidth: 190 }} />;
|
||||
}
|
||||
return (
|
||||
<div className="context-submenu" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '0.75rem', gap: '0.5rem' }}>
|
||||
<div className="context-submenu" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '0.75rem', gap: '0.5rem', minWidth: 190 }}>
|
||||
<div className="spinner" style={{ width: 16, height: 16 }} />
|
||||
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
{t('playlists.loadingArtists', { count: totalArtists })}
|
||||
@@ -609,20 +634,25 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist
|
||||
function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }: { playlist: { id: string; name: string }; onDone: () => void; triggerId?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const subRef = useRef<HTMLDivElement>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [allPlaylists, setAllPlaylists] = useState<{ id: string; name: string }[]>([]);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const newNameRef = useRef<HTMLInputElement>(null);
|
||||
const [flipLeft, setFlipLeft] = useState(false);
|
||||
const [flipUp, setFlipUp] = useState(false);
|
||||
const storePlaylists = usePlaylistStore((s) => s.playlists);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { getPlaylists } = await import('../api/subsonic');
|
||||
const pls = await getPlaylists().catch(() => []);
|
||||
setAllPlaylists(pls.filter((p: { id: string }) => p.id !== playlist.id));
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [playlist]);
|
||||
// Filter out the current playlist from the list
|
||||
const allPlaylists = useMemo(() => {
|
||||
return storePlaylists.filter((p) => p.id !== playlist.id);
|
||||
}, [storePlaylists, playlist.id]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (subRef.current) {
|
||||
const rect = subRef.current.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth - 8) setFlipLeft(true);
|
||||
if (rect.bottom > window.innerHeight - 8) setFlipUp(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (creating && newNameRef.current) {
|
||||
@@ -693,17 +723,9 @@ function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }: { play
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ minWidth: 190 }}>
|
||||
<div style={{ padding: '0.75rem', display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="spinner" style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const subStyle: React.CSSProperties = { left: 'calc(100% + 4px)', right: 'auto' };
|
||||
const subStyle: React.CSSProperties = flipLeft
|
||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||
|
||||
return (
|
||||
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ ...subStyle, minWidth: 190 }}>
|
||||
@@ -757,21 +779,26 @@ function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }: { play
|
||||
function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: { playlists: { id: string; name: string }[]; onDone: () => void; triggerId?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const subRef = useRef<HTMLDivElement>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [allPlaylists, setAllPlaylists] = useState<{ id: string; name: string }[]>([]);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const newNameRef = useRef<HTMLInputElement>(null);
|
||||
const [flipLeft, setFlipLeft] = useState(false);
|
||||
const [flipUp, setFlipUp] = useState(false);
|
||||
const storePlaylists = usePlaylistStore((s) => s.playlists);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { getPlaylists } = await import('../api/subsonic');
|
||||
const pls = await getPlaylists().catch(() => []);
|
||||
const selectedIds = new Set(playlists.map(p => p.id));
|
||||
setAllPlaylists(pls.filter((p: { id: string }) => !selectedIds.has(p.id)));
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [playlists]);
|
||||
// Filter out the selected playlists from the list
|
||||
const allPlaylists = useMemo(() => {
|
||||
const selectedIds = new Set(playlists.map(p => p.id));
|
||||
return storePlaylists.filter((p) => !selectedIds.has(p.id));
|
||||
}, [storePlaylists, playlists]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (subRef.current) {
|
||||
const rect = subRef.current.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth - 8) setFlipLeft(true);
|
||||
if (rect.bottom > window.innerHeight - 8) setFlipUp(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (creating && newNameRef.current) {
|
||||
@@ -860,17 +887,9 @@ function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: { play
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ minWidth: 190 }}>
|
||||
<div style={{ padding: '0.75rem', display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="spinner" style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const subStyle: React.CSSProperties = { left: 'calc(100% + 4px)', right: 'auto' };
|
||||
const subStyle: React.CSSProperties = flipLeft
|
||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||
|
||||
return (
|
||||
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ ...subStyle, minWidth: 190 }}>
|
||||
@@ -1064,7 +1083,7 @@ export default function ContextMenu() {
|
||||
};
|
||||
}, [pendingSubmenuKeyboardFocus, playlistSubmenuOpen, getMenuNavItems, focusMenuItemAt]);
|
||||
|
||||
const { type, item, queueIndex } = contextMenu;
|
||||
const { type, item, queueIndex, playlistId, playlistSongIndex } = contextMenu;
|
||||
|
||||
const isStarred = (id: string, itemStarred?: string) =>
|
||||
id in starredOverrides ? starredOverrides[id] : !!itemStarred;
|
||||
@@ -1485,6 +1504,26 @@ export default function ContextMenu() {
|
||||
<div className="context-menu-item" onClick={() => handleAction(() => openSongInfo(song.id))}>
|
||||
<Info size={14} /> {t('contextMenu.songInfo')}
|
||||
</div>
|
||||
{playlistId && playlistSongIndex !== undefined && (
|
||||
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(async () => {
|
||||
const { getPlaylist, updatePlaylist } = await import('../api/subsonic');
|
||||
const { usePlaylistStore } = await import('../store/playlistStore');
|
||||
const { showToast } = await import('../utils/toast');
|
||||
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
|
||||
try {
|
||||
const { songs } = await getPlaylist(playlistId);
|
||||
const prevCount = songs.length;
|
||||
const updatedIds = songs.filter((_, i) => i !== playlistSongIndex).map(s => s.id);
|
||||
await updatePlaylist(playlistId, updatedIds, prevCount);
|
||||
touchPlaylist(playlistId);
|
||||
showToast(t('playlists.removeSuccess'), 3000, 'info');
|
||||
} catch {
|
||||
showToast(t('playlists.removeError'), 4000, 'error');
|
||||
}
|
||||
})}>
|
||||
<Trash2 size={14} /> {t('contextMenu.removeFromPlaylist')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -106,6 +106,7 @@ export const deTranslation = {
|
||||
unfavoriteArtist: 'Künstler aus Favoriten entfernen',
|
||||
unfavoriteAlbum: 'Album aus Favoriten entfernen',
|
||||
removeFromQueue: 'Diesen Song entfernen',
|
||||
removeFromPlaylist: 'Aus Playlist entfernen',
|
||||
openAlbum: 'Album öffnen',
|
||||
goToArtist: 'Zum Künstler',
|
||||
download: 'Herunterladen (ZIP)',
|
||||
@@ -947,12 +948,15 @@ export const deTranslation = {
|
||||
select: 'Auswählen',
|
||||
startSelect: 'Auswahl aktivieren',
|
||||
cancelSelect: 'Abbrechen',
|
||||
loadingAlbums: '{{count}} Alben werden aufgelöst…',
|
||||
loadingArtists: '{{count}} Künstler werden aufgelöst…',
|
||||
myPlaylists: 'Meine Playlists',
|
||||
noOtherPlaylists: 'Keine anderen Playlists verfügbar',
|
||||
addToPlaylistSuccess: '{{count}} Songs zu {{playlist}} hinzugefügt',
|
||||
addToPlaylistNoNew: 'Keine neuen Songs für {{playlist}}',
|
||||
addToPlaylistError: 'Fehler beim Hinzufügen zur Playlist',
|
||||
removeSuccess: 'Song aus Playlist entfernt',
|
||||
removeError: 'Fehler beim Entfernen aus der Playlist',
|
||||
},
|
||||
mostPlayed: {
|
||||
title: 'Meistgehört',
|
||||
|
||||
@@ -107,6 +107,7 @@ export const enTranslation = {
|
||||
unfavoriteArtist: 'Remove Artist from Favorites',
|
||||
unfavoriteAlbum: 'Remove Album from Favorites',
|
||||
removeFromQueue: 'Remove from Queue',
|
||||
removeFromPlaylist: 'Remove from Playlist',
|
||||
openAlbum: 'Open Album',
|
||||
goToArtist: 'Go to Artist',
|
||||
download: 'Download (ZIP)',
|
||||
@@ -949,12 +950,15 @@ export const enTranslation = {
|
||||
select: 'Select',
|
||||
startSelect: 'Enable selection',
|
||||
cancelSelect: 'Cancel',
|
||||
loadingAlbums: 'Resolving {{count}} albums…',
|
||||
loadingArtists: 'Resolving {{count}} artists…',
|
||||
myPlaylists: 'My Playlists',
|
||||
noOtherPlaylists: 'No other playlists available',
|
||||
addToPlaylistSuccess: '{{count}} songs added to {{playlist}}',
|
||||
addToPlaylistNoNew: 'No new songs to add to {{playlist}}',
|
||||
addToPlaylistError: 'Error adding to playlist',
|
||||
removeSuccess: 'Song removed from playlist',
|
||||
removeError: 'Error removing song from playlist',
|
||||
},
|
||||
mostPlayed: {
|
||||
title: 'Most Played',
|
||||
|
||||
@@ -107,6 +107,7 @@ export const esTranslation = {
|
||||
unfavoriteArtist: 'Quitar Artista de Favoritos',
|
||||
unfavoriteAlbum: 'Quitar Álbum de Favoritos',
|
||||
removeFromQueue: 'Quitar de la Cola',
|
||||
removeFromPlaylist: 'Quitar de la Playlist',
|
||||
openAlbum: 'Abrir Álbum',
|
||||
goToArtist: 'Ir al Artista',
|
||||
download: 'Descargar (ZIP)',
|
||||
@@ -950,12 +951,15 @@ export const esTranslation = {
|
||||
select: 'Seleccionar',
|
||||
startSelect: 'Activar selección',
|
||||
cancelSelect: 'Cancelar',
|
||||
loadingAlbums: 'Resolviendo {{count}} álbumes…',
|
||||
loadingArtists: 'Resolviendo {{count}} artistas…',
|
||||
myPlaylists: 'Mis Listas',
|
||||
noOtherPlaylists: 'No hay otras listas disponibles',
|
||||
addToPlaylistSuccess: '{{count}} canciones agregadas a {{playlist}}',
|
||||
addToPlaylistNoNew: 'No hay canciones nuevas para agregar a {{playlist}}',
|
||||
addToPlaylistError: 'Error al agregar a la lista',
|
||||
removeSuccess: 'Canción quitada de la playlist',
|
||||
removeError: 'Error al quitar la canción de la playlist',
|
||||
},
|
||||
mostPlayed: {
|
||||
title: 'Más Reproducidos',
|
||||
|
||||
@@ -106,6 +106,7 @@ export const frTranslation = {
|
||||
unfavoriteArtist: 'Retirer l\'artiste des favoris',
|
||||
unfavoriteAlbum: 'Retirer l\'album des favoris',
|
||||
removeFromQueue: 'Retirer de la file',
|
||||
removeFromPlaylist: 'Retirer de la playlist',
|
||||
openAlbum: 'Ouvrir l\'album',
|
||||
goToArtist: 'Aller à l\'artiste',
|
||||
download: 'Télécharger (ZIP)',
|
||||
@@ -945,12 +946,15 @@ export const frTranslation = {
|
||||
select: 'Sélectionner',
|
||||
startSelect: 'Activer la sélection',
|
||||
cancelSelect: 'Annuler',
|
||||
loadingAlbums: 'Résolution de {{count}} albums…',
|
||||
loadingArtists: 'Résolution de {{count}} artistes…',
|
||||
myPlaylists: 'Mes Playlists',
|
||||
noOtherPlaylists: 'Aucune autre playlist disponible',
|
||||
addToPlaylistSuccess: '{{count}} morceaux ajoutés à {{playlist}}',
|
||||
addToPlaylistNoNew: 'Aucun nouveau morceau pour {{playlist}}',
|
||||
addToPlaylistError: 'Erreur lors de l\'ajout à la playlist',
|
||||
removeSuccess: 'Morceau retiré de la playlist',
|
||||
removeError: 'Erreur lors du retrait de la playlist',
|
||||
},
|
||||
mostPlayed: {
|
||||
title: 'Les plus joués',
|
||||
|
||||
@@ -106,6 +106,7 @@ export const nbTranslation = {
|
||||
unfavoriteArtist: 'Fjern artist fra favoritter',
|
||||
unfavoriteAlbum: 'Fjern album fra favoritter',
|
||||
removeFromQueue: 'Fjern fra kø',
|
||||
removeFromPlaylist: 'Fjern fra spilleliste',
|
||||
openAlbum: 'Åpne album',
|
||||
goToArtist: 'Gå til artist',
|
||||
download: 'Last ned (ZIP)',
|
||||
@@ -944,12 +945,15 @@ export const nbTranslation = {
|
||||
select: 'Velg',
|
||||
startSelect: 'Aktiver valg',
|
||||
cancelSelect: 'Avbryt',
|
||||
loadingAlbums: 'Løser {{count}} album…',
|
||||
loadingArtists: 'Løser {{count}} artister…',
|
||||
myPlaylists: 'Mine spillelister',
|
||||
noOtherPlaylists: 'Ingen andre spillelister tilgjengelig',
|
||||
addToPlaylistSuccess: '{{count}} sanger lagt til i {{playlist}}',
|
||||
addToPlaylistNoNew: 'Ingen nye sanger for {{playlist}}',
|
||||
addToPlaylistError: 'Feil ved å legge til i spilleliste',
|
||||
removeSuccess: 'Sang fjernet fra spilleliste',
|
||||
removeError: 'Feil ved fjerning fra spilleliste',
|
||||
},
|
||||
mostPlayed: {
|
||||
title: 'Mest spilt',
|
||||
|
||||
@@ -945,6 +945,7 @@ export const nlTranslation = {
|
||||
select: 'Selecteren',
|
||||
startSelect: 'Selectie inschakelen',
|
||||
cancelSelect: 'Annuleren',
|
||||
loadingAlbums: '{{count}} albums oplossen…',
|
||||
loadingArtists: '{{count}} artiesten oplossen…',
|
||||
myPlaylists: 'Mijn playlists',
|
||||
noOtherPlaylists: 'Geen andere playlists beschikbaar',
|
||||
|
||||
@@ -107,6 +107,7 @@ export const ruTranslation = {
|
||||
unfavoriteArtist: 'Убрать исполнителя из избранного',
|
||||
unfavoriteAlbum: 'Убрать альбом из избранного',
|
||||
removeFromQueue: 'Убрать из очереди',
|
||||
removeFromPlaylist: 'Убрать из плейлиста',
|
||||
openAlbum: 'Открыть альбом',
|
||||
goToArtist: 'К исполнителю',
|
||||
download: 'Скачать (ZIP)',
|
||||
@@ -1004,12 +1005,15 @@ export const ruTranslation = {
|
||||
select: 'Выбрать',
|
||||
startSelect: 'Включить выбор',
|
||||
cancelSelect: 'Отмена',
|
||||
loadingAlbums: 'Загрузка {{count}} альбомов…',
|
||||
loadingArtists: 'Загрузка {{count}} исполнителей…',
|
||||
myPlaylists: 'Мои плейлисты',
|
||||
noOtherPlaylists: 'Нет других доступных плейлистов',
|
||||
addToPlaylistSuccess: '{{count}} треков добавлено в {{playlist}}',
|
||||
addToPlaylistNoNew: 'Нет новых треков для {{playlist}}',
|
||||
addToPlaylistError: 'Ошибка при добавлении в плейлист',
|
||||
removeSuccess: 'Трек убран из плейлиста',
|
||||
removeError: 'Ошибка при удалении из плейлиста',
|
||||
},
|
||||
mostPlayed: {
|
||||
title: 'Популярное',
|
||||
|
||||
@@ -941,6 +941,7 @@ export const zhTranslation = {
|
||||
select: '选择',
|
||||
startSelect: '启用选择',
|
||||
cancelSelect: '取消',
|
||||
loadingAlbums: '正在解析 {{count}} 张专辑…',
|
||||
loadingArtists: '正在解析 {{count}} 位艺术家…',
|
||||
myPlaylists: '我的播放列表',
|
||||
noOtherPlaylists: '没有其他可用播放列表',
|
||||
|
||||
@@ -234,6 +234,8 @@ export default function PlaylistDetail() {
|
||||
}, [contextMenuOpen]);
|
||||
|
||||
// ── Load ─────────────────────────────────────────────────────
|
||||
const lastModified = usePlaylistStore(s => (id ? s.lastModified[id] : undefined));
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
@@ -253,7 +255,7 @@ export default function PlaylistDetail() {
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
}, [id, lastModified]);
|
||||
|
||||
// ── Suggestions ───────────────────────────────────────────────
|
||||
const loadSuggestions = useCallback(async (currentSongs: SubsonicSong[]) => {
|
||||
@@ -1019,7 +1021,7 @@ export default function PlaylistDetail() {
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
setContextMenuSongId(song.id);
|
||||
openContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song');
|
||||
openContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song', undefined, id, realIdx);
|
||||
}}
|
||||
>
|
||||
{visibleCols.map(colDef => {
|
||||
|
||||
@@ -176,8 +176,10 @@ interface PlayerState {
|
||||
item: any;
|
||||
type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist' | null;
|
||||
queueIndex?: number;
|
||||
playlistId?: string;
|
||||
playlistSongIndex?: number;
|
||||
};
|
||||
openContextMenu: (x: number, y: number, item: any, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist', queueIndex?: number) => void;
|
||||
openContextMenu: (x: number, y: number, item: any, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist', queueIndex?: number, playlistId?: string, playlistSongIndex?: number) => void;
|
||||
closeContextMenu: () => void;
|
||||
|
||||
songInfoModal: { isOpen: boolean; songId: string | null };
|
||||
@@ -729,8 +731,8 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
repeatMode: 'off',
|
||||
contextMenu: { isOpen: false, x: 0, y: 0, item: null, type: null },
|
||||
|
||||
openContextMenu: (x, y, item, type, queueIndex) => set({
|
||||
contextMenu: { isOpen: true, x, y, item, type, queueIndex },
|
||||
openContextMenu: (x, y, item, type, queueIndex, playlistId, playlistSongIndex) => set({
|
||||
contextMenu: { isOpen: true, x, y, item, type, queueIndex, playlistId, playlistSongIndex },
|
||||
}),
|
||||
closeContextMenu: () => set(state => ({
|
||||
contextMenu: { ...state.contextMenu, isOpen: false },
|
||||
|
||||
@@ -6,6 +6,7 @@ interface PlaylistStore {
|
||||
recentIds: string[];
|
||||
playlists: SubsonicPlaylist[];
|
||||
playlistsLoading: boolean;
|
||||
lastModified: Record<string, number>;
|
||||
touchPlaylist: (id: string) => void;
|
||||
removeId: (id: string) => void;
|
||||
fetchPlaylists: () => Promise<void>;
|
||||
@@ -19,9 +20,11 @@ export const usePlaylistStore = create<PlaylistStore>()(
|
||||
recentIds: [],
|
||||
playlists: [],
|
||||
playlistsLoading: false,
|
||||
lastModified: {},
|
||||
touchPlaylist: (id) =>
|
||||
set((s) => ({
|
||||
recentIds: [id, ...s.recentIds.filter((x) => x !== id)].slice(0, 50),
|
||||
lastModified: { ...s.lastModified, [id]: Date.now() },
|
||||
})),
|
||||
removeId: (id) =>
|
||||
set((s) => ({ recentIds: s.recentIds.filter((x) => x !== id) })),
|
||||
|
||||
Reference in New Issue
Block a user