mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
feat(search): queue preview on Ctrl+V paste and modal polish
Global queue paste opens the preview modal before play; overlay scrollbar, context-menu suppression, changelog/credits for PR #716 and DanielWTE (#551).
This commit is contained in:
@@ -11,6 +11,82 @@ import { showToast } from '../ui/toast';
|
||||
|
||||
const RESOLVE_QUEUE_CHUNK = 12;
|
||||
|
||||
type SharePasteQueuePayload = Extract<EntitySharePayloadV1, { k: 'queue' }>;
|
||||
|
||||
async function resolveSharePasteQueueSongs(
|
||||
ids: string[],
|
||||
): Promise<{ songs: SubsonicSong[]; skipped: number } | null> {
|
||||
if (ids.length === 0) return null;
|
||||
|
||||
const resolved: SubsonicSong[] = [];
|
||||
for (let i = 0; i < ids.length; i += RESOLVE_QUEUE_CHUNK) {
|
||||
const chunk = ids.slice(i, i + RESOLVE_QUEUE_CHUNK);
|
||||
const songs = await Promise.all(chunk.map(id => getSong(id)));
|
||||
for (const s of songs) {
|
||||
if (s) resolved.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
const skipped = ids.length - resolved.length;
|
||||
if (resolved.length === 0) return null;
|
||||
return { songs: resolved, skipped };
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the matching server, resolves queue track ids, then replaces the
|
||||
* queue and starts playback. Used after the queue preview modal on global paste.
|
||||
*/
|
||||
export async function applySharePasteQueue(
|
||||
payload: SharePasteQueuePayload,
|
||||
t: TFunction,
|
||||
): Promise<boolean> {
|
||||
const { servers, isLoggedIn, setActiveServer } = useAuthStore.getState();
|
||||
if (!isLoggedIn) {
|
||||
showToast(t('sharePaste.notLoggedIn'), 4000, 'info');
|
||||
return false;
|
||||
}
|
||||
|
||||
const serverId = findServerIdForShareUrl(servers, payload.srv);
|
||||
if (!serverId) {
|
||||
showToast(t('sharePaste.noMatchingServer', { url: payload.srv }), 6000, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useAuthStore.getState().activeServerId !== serverId) {
|
||||
setActiveServer(serverId);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await resolveSharePasteQueueSongs(payload.ids);
|
||||
if (!result) {
|
||||
showToast(t('sharePaste.queueAllUnavailable'), 6000, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
const tracks = result.songs.map(songToTrack);
|
||||
usePlayerStore.getState().clearQueue();
|
||||
usePlayerStore.getState().playTrack(tracks[0]!, tracks);
|
||||
if (result.skipped > 0) {
|
||||
showToast(
|
||||
t('sharePaste.openedQueuePartial', {
|
||||
played: tracks.length,
|
||||
total: payload.ids.length,
|
||||
skipped: result.skipped,
|
||||
}),
|
||||
5000,
|
||||
'info',
|
||||
);
|
||||
} else {
|
||||
showToast(t('sharePaste.openedQueue', { count: tracks.length }), 3000, 'info');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('[psysonic] share paste queue failed', e);
|
||||
showToast(t('sharePaste.genericError'), 5000, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the matching server, validates the entity on the server, then
|
||||
* plays or navigates. Caller should `preventDefault` on the paste event when
|
||||
@@ -91,37 +167,7 @@ export async function applySharePastePayload(
|
||||
}
|
||||
|
||||
if (payload.k === 'queue') {
|
||||
const { ids } = payload;
|
||||
if (ids.length === 0) {
|
||||
showToast(t('sharePaste.genericError'), 5000, 'error');
|
||||
return;
|
||||
}
|
||||
const resolved: SubsonicSong[] = [];
|
||||
for (let i = 0; i < ids.length; i += RESOLVE_QUEUE_CHUNK) {
|
||||
const chunk = ids.slice(i, i + RESOLVE_QUEUE_CHUNK);
|
||||
const songs = await Promise.all(chunk.map(id => getSong(id)));
|
||||
for (let j = 0; j < songs.length; j++) {
|
||||
const s = songs[j];
|
||||
if (s) resolved.push(s);
|
||||
}
|
||||
}
|
||||
const skipped = ids.length - resolved.length;
|
||||
if (resolved.length === 0) {
|
||||
showToast(t('sharePaste.queueAllUnavailable'), 6000, 'error');
|
||||
return;
|
||||
}
|
||||
const tracks = resolved.map(songToTrack);
|
||||
usePlayerStore.getState().clearQueue();
|
||||
usePlayerStore.getState().playTrack(tracks[0]!, tracks);
|
||||
if (skipped > 0) {
|
||||
showToast(
|
||||
t('sharePaste.openedQueuePartial', { played: tracks.length, total: ids.length, skipped }),
|
||||
5000,
|
||||
'info',
|
||||
);
|
||||
} else {
|
||||
showToast(t('sharePaste.openedQueue', { count: tracks.length }), 3000, 'info');
|
||||
}
|
||||
await applySharePasteQueue(payload, t);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -23,3 +23,20 @@ export function shareServerOriginLabel(
|
||||
|
||||
return serverListDisplayLabel(server, servers);
|
||||
}
|
||||
|
||||
/** Server label and profile for queue preview when the link targets another saved server. */
|
||||
export function shareQueueServerContext(
|
||||
shareSrv: string,
|
||||
servers: ServerProfile[],
|
||||
activeServerId: string | null,
|
||||
): { label: string | null; coverServer: ServerProfile | null } {
|
||||
const match: ShareSearchMatch = {
|
||||
type: 'queueable',
|
||||
payload: { srv: shareSrv, k: 'queue', ids: [] },
|
||||
};
|
||||
const label = shareServerOriginLabel(match, servers, activeServerId);
|
||||
const serverId = findServerIdForShareUrl(servers, shareSrv);
|
||||
const coverServer =
|
||||
serverId && serverId !== activeServerId ? servers.find(s => s.id === serverId) ?? null : null;
|
||||
return { label, coverServer };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user