diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e248e14..f70361c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed +### Queue toolbar — Navidrome public share sessions + +**By [@cucadmuh](https://github.com/cucadmuh), PR [#1279](https://github.com/Psychotoxical/psysonic/pull/1279)** + +* While a Navidrome public share queue is active, **Save Playlist** is hidden in the queue toolbar (share tracks cannot be saved to the server). **Load Playlist** stays available. +* The queue **Share** button copies the original Navidrome `/share/{id}` page URL instead of a Psysonic magic-string queue payload. + ### Equalizer — per-device profiles follow the active system default **By [@cucadmuh](https://github.com/cucadmuh), PR [#1274](https://github.com/Psychotoxical/psysonic/pull/1274)**, suggested by [@JustBuddy](https://github.com/JustBuddy) diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index c6b99d74..ed1a0126 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -197,6 +197,7 @@ const CONTRIBUTOR_ENTRIES = [ 'Servers — connect and run fully behind a custom-header gate (Cloudflare Access / Pangolin): native connect probe + REST proxy so browse/playback/covers pass, add-form failure reasons, live LAN/public badge on server switch, and LAN reclaim from a sticky public endpoint (PR #1273)', 'Per-device EQ — when System Default is selected, profiles follow the active OS default output and switch on external output changes (PR #1274)', 'Navidrome public share links — paste/search preview modal, anonymous queue playback, idle server queue-restore guard (PR #1275)', + 'Queue toolbar — Navidrome public share: hide Save Playlist, copy original share URL (PR #1279)', 'Windows startup hang after #1274 — boot barrel split, stable Wasapi device IDs, legacy EQ key match (PR #1277)', 'Windows MSI bundle on dev/RC versions — numeric WiX mapping; album easter-egg import chunk (PR #1278)', ], diff --git a/src/features/playback/store/playTrackAction.ts b/src/features/playback/store/playTrackAction.ts index 2f920099..f9f554ad 100644 --- a/src/features/playback/store/playTrackAction.ts +++ b/src/features/playback/store/playTrackAction.ts @@ -64,6 +64,7 @@ import type { PlayerState } from '@/features/playback/store/playerStoreTypes'; import { toQueueItemRefs } from '@/features/playback/store/queueItemRef'; import { getQueueTracksView, resolveQueueTrack } from '@/features/playback/store/queueTrackView'; import { getCachedTrack, seedQueueResolver, mergeDirectShareUrls } from '@/features/playback/store/queueTrackResolver'; +import { tracksArePublicShareQueue } from '@/lib/share/navidromePublicSharePlayback'; import { promoteCompletedStreamToHotCache } from '@/features/playback/store/promoteStreamCache'; import { pushQueueOnPlaybackStart } from '@/features/playback/store/queueSync'; import { playListenSessionFinalize } from '@/features/playback/store/playListenSession'; @@ -260,6 +261,7 @@ export function runPlayTrack( // every subscriber on each track change at scale. const replacing = scopedQueue !== undefined; const srcLen = replacing ? scopedQueue.length : state.queueItems.length; + const dropSharePageUrl = replacing && scopedQueue && !tracksArePublicShareQueue(scopedQueue); if (replacing && shouldBindQueueServerForPlay(state.queueItems, scopedQueue, scopedQueue)) { bindQueueServerForTracks(scopedQueue); } @@ -428,6 +430,7 @@ export function runPlayTrack( set({ currentRadio: null, ...(replacing ? { queueItems: toQueueItemRefs(queueSid, scopedQueue) } : {}), + ...(dropSharePageUrl ? { navidromePublicSharePageUrl: null } : {}), queueIndex: idx >= 0 ? idx : 0, }); } else { @@ -439,6 +442,7 @@ export function runPlayTrack( ...deriveNormalizationSnapshot(trackForPlay, playNormWindow, normIdx), // Only a replace rewrites the queue; navigation keeps the canonical refs. ...(replacing ? { queueItems: toQueueItemRefs(queueSid, scopedQueue) } : {}), + ...(dropSharePageUrl ? { navidromePublicSharePageUrl: null } : {}), queueIndex: idx >= 0 ? idx : 0, progress: initialProgress, buffered: 0, diff --git a/src/features/playback/store/playerStore.ts b/src/features/playback/store/playerStore.ts index e69796cb..d15cb828 100644 --- a/src/features/playback/store/playerStore.ts +++ b/src/features/playback/store/playerStore.ts @@ -54,6 +54,7 @@ export const usePlayerStore = create()( // through the resolver. `currentTrack` stays a full resolved singleton. queueItems: [], queueServerId: null, + navidromePublicSharePageUrl: null, queueIndex: 0, isPlaying: false, isPlaybackBuffering: false, diff --git a/src/features/playback/store/playerStoreTypes.ts b/src/features/playback/store/playerStoreTypes.ts index 93e1747a..c732084b 100644 --- a/src/features/playback/store/playerStoreTypes.ts +++ b/src/features/playback/store/playerStoreTypes.ts @@ -24,6 +24,8 @@ export interface PlayerState { enginePreloadedTrackId: string | null; /** Saved server for stream/hot-cache/offline resolution while this queue plays. */ queueServerId: string | null; + /** Navidrome public share page URL for the live queue session (not persisted). */ + navidromePublicSharePageUrl: string | null; queueIndex: number; /** F5 (transient): full ordered track-id list + index persisted alongside the * windowed `queue`. On startup, when the library index is ready, the whole diff --git a/src/features/playback/store/queueMutationActions.ts b/src/features/playback/store/queueMutationActions.ts index 0bb85ec8..7107d963 100644 --- a/src/features/playback/store/queueMutationActions.ts +++ b/src/features/playback/store/queueMutationActions.ts @@ -231,7 +231,16 @@ export function createQueueMutationActions(set: SetState, get: GetState): Pick< setCurrentRadioArtistId(null); clearTimelineSessionHistory(); clearQueueServerForPlayback(); - set({ queueItems: [], queueIndex: 0, currentTrack: null, isPlaying: false, progress: 0, buffered: 0, currentTime: 0 }); + set({ + queueItems: [], + queueIndex: 0, + currentTrack: null, + navidromePublicSharePageUrl: null, + isPlaying: false, + progress: 0, + buffered: 0, + currentTime: 0, + }); syncUserQueueMutationToServer([], null, 0); }, diff --git a/src/features/queue/components/QueuePanel.tsx b/src/features/queue/components/QueuePanel.tsx index bc4db60b..9864b56e 100644 --- a/src/features/queue/components/QueuePanel.tsx +++ b/src/features/queue/components/QueuePanel.tsx @@ -35,6 +35,7 @@ import { useQueueAutoScroll } from '@/features/queue/hooks/useQueueAutoScroll'; import { useTimelineBootstrapOnMode, useTimelineHistoryResolver, useTimelinePlayHistory } from '@/features/playback/hooks/useTimelinePlayHistory'; import { buildTimelineDisplayRows } from '@/features/playback/utils/buildTimelineDisplayRows'; import { activeServerQueueTrackIds } from '@/features/playback/utils/playback/trackServerScope'; +import { isActivePublicShareQueue } from '@/lib/share/navidromePublicSharePlayback'; export default function QueuePanel() { const orbitRole = useOrbitStore(s => s.role); @@ -79,6 +80,9 @@ function QueuePanelHostOrSolo() { // Track from the resolver. List, header, toolbar and id/length reads (save / // share / playlist) all read off the refs. const queueItems = usePlayerStore(s => s.queueItems); + const queueServerId = usePlayerStore(s => s.queueServerId); + const navidromePublicSharePageUrl = usePlayerStore(s => s.navidromePublicSharePageUrl); + const publicShareQueueActive = isActivePublicShareQueue(queueServerId, queueItems); const queueIndex = usePlayerStore(s => s.queueIndex); const currentTrack = usePlayerStore(s => s.currentTrack); const userRatingOverrides = usePlayerStore(s => s.userRatingOverrides); @@ -182,6 +186,7 @@ function QueuePanelHostOrSolo() { const [loadModalOpen, setLoadModalOpen] = useState(false); const handleSave = async () => { + if (publicShareQueueActive) return; const exportTrackIds = activeServerQueueTrackIds(queueItems); if (exportTrackIds.length === 0) return; if (activePlaylist) { @@ -209,6 +214,17 @@ function QueuePanelHostOrSolo() { }; const handleCopyQueueShare = async () => { + if (publicShareQueueActive) { + const pageUrl = navidromePublicSharePageUrl?.trim(); + if (!pageUrl) { + showToast(t('queue.shareNavidromePublicMissing'), 4000, 'error'); + return; + } + const ok = await copyTextToClipboard(pageUrl); + if (ok) showToast(t('contextMenu.shareCopied')); + else showToast(t('contextMenu.shareCopyFailed'), 4000, 'error'); + return; + } const ids = activeServerQueueTrackIds(queueItems); if (ids.length === 0) { showToast(t('queue.shareQueueEmpty'), 3000, 'info'); @@ -344,6 +360,7 @@ function QueuePanelHostOrSolo() { handleLoad={handleLoad} handleCopyQueueShare={handleCopyQueueShare} handleClear={handleClear} + publicShareQueueActive={publicShareQueueActive} gaplessEnabled={gaplessEnabled} crossfadeEnabled={crossfadeEnabled} crossfadeTrimSilence={crossfadeTrimSilence} diff --git a/src/features/queue/components/QueueToolbar.tsx b/src/features/queue/components/QueueToolbar.tsx index 659ee749..73b78a5b 100644 --- a/src/features/queue/components/QueueToolbar.tsx +++ b/src/features/queue/components/QueueToolbar.tsx @@ -21,6 +21,7 @@ interface Props { handleLoad: () => void; handleCopyQueueShare: () => void; handleClear: () => void; + publicShareQueueActive: boolean; gaplessEnabled: boolean; crossfadeEnabled: boolean; crossfadeTrimSilence: boolean; @@ -34,6 +35,7 @@ interface Props { export function QueueToolbar({ queue, activePlaylist, saveState, toolbarButtons, shuffleQueue, handleSave, handleLoad, handleCopyQueueShare, handleClear, + publicShareQueueActive, gaplessEnabled, crossfadeEnabled, crossfadeTrimSilence, crossfadeSecs, setCrossfadeSecs, infiniteQueueEnabled, setInfiniteQueueEnabled, @@ -99,15 +101,17 @@ export function QueueToolbar({ {showPlaylistMenu && (
- + {!publicShareQueueActive && ( + + )} diff --git a/src/features/queue/components/queuePublicShareToolbar.test.tsx b/src/features/queue/components/queuePublicShareToolbar.test.tsx new file mode 100644 index 00000000..dbd4ee2f --- /dev/null +++ b/src/features/queue/components/queuePublicShareToolbar.test.tsx @@ -0,0 +1,82 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import QueuePanel from '@/features/queue/components/QueuePanel'; +import { renderWithProviders } from '@/test/helpers/renderWithProviders'; +import { usePlayerStore } from '@/features/playback/store/playerStore'; +import { useAuthStore } from '@/store/authStore'; +import { resetAllStores } from '@/test/helpers/storeReset'; +import { makeTrack } from '@/test/helpers/factories'; +import { onInvoke, registerDefaultCoverInvokeHandlers } from '@/test/mocks/tauri'; + +const copyTextToClipboardMock = vi.fn(async (_text: string) => true); + +vi.mock('@/lib/server/serverMagicString', () => ({ + copyTextToClipboard: (text: string) => copyTextToClipboardMock(text), +})); + +vi.mock('@/features/orbit/utils/orbitBulkGuard', () => ({ + orbitBulkGuard: vi.fn(async () => true), +})); + +vi.mock('@/lib/api/subsonic', () => ({ + savePlayQueue: vi.fn(async () => undefined), + getPlayQueue: vi.fn(async () => ({ songs: [], current: undefined, position: 0 })), + buildStreamUrl: vi.fn((id: string) => `https://mock/stream/${id}`), + buildCoverArtUrl: vi.fn((id: string) => `https://mock/cover/${id}`), + getSong: vi.fn(async () => null), +})); + +function seedPublicShareQueue(pageUrl: string) { + const track = { + ...makeTrack({ id: 'ndshare:AbCdEfGhIj:0', serverId: 'navidrome-public-share' }), + directStreamUrl: 'https://music.test/share/s/jwt-token', + }; + usePlayerStore.setState({ + queueServerId: 'navidrome-public-share', + navidromePublicSharePageUrl: pageUrl, + queueItems: [{ + serverId: 'navidrome-public-share', + trackId: 'ndshare:AbCdEfGhIj:0', + directStreamUrl: track.directStreamUrl, + }], + queueIndex: 0, + currentTrack: track, + }); +} + +describe('QueuePanel public share toolbar', () => { + beforeEach(() => { + resetAllStores(); + copyTextToClipboardMock.mockClear(); + const id = useAuthStore.getState().addServer({ + name: 'T', url: 'https://x.test', username: 'u', password: 'p', + }); + useAuthStore.getState().setActiveServer(id); + registerDefaultCoverInvokeHandlers(); + onInvoke('audio_play', () => undefined); + onInvoke('audio_pause', () => undefined); + onInvoke('audio_stop', () => undefined); + onInvoke('audio_seek', () => undefined); + onInvoke('audio_get_state', () => ({ playing: false })); + onInvoke('audio_update_replay_gain', () => undefined); + onInvoke('discord_update_presence', () => undefined); + onInvoke('library_get_recent_play_sessions', () => []); + }); + + it('hides Save Playlist in the playlist menu for a Navidrome public share queue', () => { + seedPublicShareQueue('https://music.test/share/AbCdEfGhIj'); + const { getByLabelText, container } = renderWithProviders(); + fireEvent.click(getByLabelText('Playlist')); + const menu = container.querySelector('.queue-menu'); + expect(menu?.textContent).not.toContain('Save Playlist'); + expect(menu?.textContent).toContain('Load Playlist'); + }); + + it('copies the original Navidrome share page URL from the share button', async () => { + const pageUrl = 'https://music.test/share/AbCdEfGhIj'; + seedPublicShareQueue(pageUrl); + const { getByLabelText } = renderWithProviders(); + fireEvent.click(getByLabelText('Copy Navidrome share link')); + expect(copyTextToClipboardMock).toHaveBeenCalledWith(pageUrl); + }); +}); diff --git a/src/features/share/playNavidromePublicShare.test.ts b/src/features/share/playNavidromePublicShare.test.ts new file mode 100644 index 00000000..655db21f --- /dev/null +++ b/src/features/share/playNavidromePublicShare.test.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { usePlayerStore } from '@/features/playback/store/playerStore'; +import { playNavidromePublicShare } from '@/features/share/playNavidromePublicShare'; +import type { NavidromePublicShareInfo } from '@/lib/share/navidromePublicShareTypes'; +import { resetPlayerStore } from '@/test/helpers/storeReset'; +import { onInvoke, registerDefaultCoverInvokeHandlers } from '@/test/mocks/tauri'; + +vi.mock('@/lib/dom/toast', () => ({ + showToast: vi.fn(), +})); + +vi.mock('@/features/playback/store/queueTrackResolver', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + seedQueueResolver: vi.fn(), + }; +}); + +const shareRef = { + pageUrl: 'https://music.test/share/AbCdEfGhIj', + origin: 'https://music.test', + basePath: '', + shareId: 'AbCdEfGhIj', +}; + +const shareInfo: NavidromePublicShareInfo = { + id: 'AbCdEfGhIj', + description: '', + downloadable: false, + tracks: [{ + id: 'jwt-token', + title: 'Track', + artist: 'Artist', + album: 'Album', + duration: 180, + }], +}; + +describe('playNavidromePublicShare', () => { + beforeEach(() => { + resetPlayerStore(); + registerDefaultCoverInvokeHandlers(); + onInvoke('audio_play', () => undefined); + onInvoke('audio_pause', () => undefined); + onInvoke('audio_stop', () => undefined); + onInvoke('audio_seek', () => undefined); + onInvoke('audio_get_state', () => ({ playing: false })); + onInvoke('audio_update_replay_gain', () => undefined); + onInvoke('discord_update_presence', () => undefined); + onInvoke('library_get_recent_play_sessions', () => []); + }); + + it('stores the Navidrome share page URL for queue toolbar copy', async () => { + const t = ((key: string) => key) as never; + const ok = await playNavidromePublicShare(shareRef, shareInfo, t); + expect(ok).toBe(true); + expect(usePlayerStore.getState().navidromePublicSharePageUrl).toBe(shareRef.pageUrl); + }); +}); diff --git a/src/features/share/playNavidromePublicShare.ts b/src/features/share/playNavidromePublicShare.ts index 6bce0750..f14c52bd 100644 --- a/src/features/share/playNavidromePublicShare.ts +++ b/src/features/share/playNavidromePublicShare.ts @@ -18,6 +18,7 @@ export async function playNavidromePublicShare( usePlayerStore.getState().clearQueue(); seedQueueResolver(NAVIDROME_PUBLIC_SHARE_SERVER_ID, tracks); + usePlayerStore.setState({ navidromePublicSharePageUrl: ref.pageUrl }); usePlayerStore.getState().playTrack(tracks[0]!, tracks); showToast(t('sharePaste.openedNavidromePublic', { count: tracks.length }), 3000, 'info'); return true; diff --git a/src/lib/share/navidromePublicSharePlayback.ts b/src/lib/share/navidromePublicSharePlayback.ts index 1ac924de..e725999b 100644 --- a/src/lib/share/navidromePublicSharePlayback.ts +++ b/src/lib/share/navidromePublicSharePlayback.ts @@ -47,3 +47,12 @@ export function isPublicSharePersistedTrack(track: Track | null | undefined): bo if (!track) return false; return track.serverId === NAVIDROME_PUBLIC_SHARE_SERVER_ID || isPublicShareTrackId(track.id); } + +export function tracksArePublicShareQueue( + tracks: ReadonlyArray>, +): boolean { + return tracks.length > 0 + && tracks.every(t => + t.serverId === NAVIDROME_PUBLIC_SHARE_SERVER_ID || isPublicShareTrackId(t.id), + ); +} diff --git a/src/locales/bg/queue.ts b/src/locales/bg/queue.ts index a8567f77..59ca0b7c 100644 --- a/src/locales/bg/queue.ts +++ b/src/locales/bg/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Затвори', nextTracks: 'Следващи песни', shareQueue: 'Копирай връзка за споделяне на опашката', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'Опашката е празна — няма какво да се сподели.', emptyQueue: 'Опашката е празна.', crossServerEnqueueBlocked: 'Песни от друг сървър не могат да се добавят към текущата опашка. Първо завършете или изчистете опашката.', diff --git a/src/locales/de/queue.ts b/src/locales/de/queue.ts index 9d2aacc1..bb81c2d3 100644 --- a/src/locales/de/queue.ts +++ b/src/locales/de/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Schließen', nextTracks: 'Nächste Titel', shareQueue: 'Warteschlangen-Link kopieren', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'Die Warteschlange ist leer — nichts zu teilen.', emptyQueue: 'Die Warteschlange ist leer.', crossServerEnqueueBlocked: 'Titel von einem anderen Server können der aktuellen Warteschlange nicht hinzugefügt werden. Beende oder leere die Warteschlange zuerst.', diff --git a/src/locales/en/queue.ts b/src/locales/en/queue.ts index 5c427cdc..2ad65a45 100644 --- a/src/locales/en/queue.ts +++ b/src/locales/en/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Close', nextTracks: 'Next Tracks', shareQueue: 'Copy queue share link', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'The queue is empty — nothing to share.', emptyQueue: 'The queue is empty.', crossServerEnqueueBlocked: 'Tracks from another server cannot be added to the current queue. Finish or clear the queue first.', diff --git a/src/locales/es/queue.ts b/src/locales/es/queue.ts index 7f6d81c3..0a42d8c8 100644 --- a/src/locales/es/queue.ts +++ b/src/locales/es/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Cerrar', nextTracks: 'Siguientes', shareQueue: 'Copiar enlace de la cola', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'La cola está vacía — no hay nada que compartir.', emptyQueue: 'La cola está vacía.', crossServerEnqueueBlocked: 'No se pueden añadir pistas de otro servidor a la cola actual. Termina o vacía la cola primero.', diff --git a/src/locales/fr/queue.ts b/src/locales/fr/queue.ts index 320dffb3..4a11cd19 100644 --- a/src/locales/fr/queue.ts +++ b/src/locales/fr/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Fermer', nextTracks: 'Pistes suivantes', shareQueue: 'Copier le lien de la file d’attente', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'La file d’attente est vide — rien à partager.', emptyQueue: 'La file d\'attente est vide.', crossServerEnqueueBlocked: 'Les titres d\'un autre serveur ne peuvent pas être ajoutés à la file en cours. Terminez ou videz la file d\'abord.', diff --git a/src/locales/hu/queue.ts b/src/locales/hu/queue.ts index 6a73632d..df0203dc 100644 --- a/src/locales/hu/queue.ts +++ b/src/locales/hu/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Bezárás', nextTracks: 'Következő számok', shareQueue: 'Sor megosztási linkjének másolása', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'A sor üres — nincs mit megosztani.', emptyQueue: 'A sor üres.', crossServerEnqueueBlocked: 'Másik szerverről származó számok nem adhatók a jelenlegi sorhoz. Előbb fejezd be vagy töröld a sort.', diff --git a/src/locales/it/queue.ts b/src/locales/it/queue.ts index 957529f1..a59551d6 100644 --- a/src/locales/it/queue.ts +++ b/src/locales/it/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Chiudi', nextTracks: 'Prossimi brani', shareQueue: 'Copia il link di condivisione della coda', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'La coda è vuota — niente da condividere.', emptyQueue: 'La coda è vuota.', crossServerEnqueueBlocked: 'Non possono essere aggiungti brani alla coda attuale da un altro server. Termina o svuota la coda.', diff --git a/src/locales/ja/queue.ts b/src/locales/ja/queue.ts index 9087d3ab..1f1ec1b1 100644 --- a/src/locales/ja/queue.ts +++ b/src/locales/ja/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: '閉じる', nextTracks: '次のトラック', shareQueue: 'キューの共有リンクをコピー', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'キューは空です。共有できるトラックはありません。', emptyQueue: 'キューは空です。', crossServerEnqueueBlocked: '別サーバーのトラックは現在のキューへ追加できません。先にキューを再生し終えるかクリアしてください。', diff --git a/src/locales/nb/queue.ts b/src/locales/nb/queue.ts index ecf9dbad..94ecfabd 100644 --- a/src/locales/nb/queue.ts +++ b/src/locales/nb/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Lukk', nextTracks: 'Neste spor', shareQueue: 'Kopiér lenke til kø', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'Køen er tom — ingenting å dele.', emptyQueue: 'Køen er tom.', crossServerEnqueueBlocked: 'Spor fra en annen server kan ikke legges til i den nåværende køen. Tøm eller avslutt køen først.', diff --git a/src/locales/nl/queue.ts b/src/locales/nl/queue.ts index 6903e373..995befca 100644 --- a/src/locales/nl/queue.ts +++ b/src/locales/nl/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Sluiten', nextTracks: 'Volgende nummers', shareQueue: 'Wachtrij-deellink kopiëren', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'De wachtrij is leeg — niets om te delen.', emptyQueue: 'De wachtrij is leeg.', crossServerEnqueueBlocked: 'Tracks van een andere server kunnen niet aan de huidige wachtrij worden toegevoegd. Maak de wachtrij eerst leeg of stop afspelen.', diff --git a/src/locales/pl/queue.ts b/src/locales/pl/queue.ts index 04a3d5ae..d28d503b 100644 --- a/src/locales/pl/queue.ts +++ b/src/locales/pl/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Zamknij', nextTracks: 'Następne utwory', shareQueue: 'Skopiuj link do udostępnienia kolejki', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'Kolejka jest pusta — nie ma czego udostępnić.', emptyQueue: 'Kolejka jest pusta.', crossServerEnqueueBlocked: 'Nie można dodać do bieżącej kolejki utworów z innego serwera. Najpierw zakończ lub wyczyść kolejkę.', diff --git a/src/locales/ro/queue.ts b/src/locales/ro/queue.ts index 0e37dc4c..1b4f9ef5 100644 --- a/src/locales/ro/queue.ts +++ b/src/locales/ro/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Închide', nextTracks: 'Următoarele Piese', shareQueue: 'Copiază link-ul distribuirii cozii', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: 'Coada este goală — nimic de distribuit.', emptyQueue: 'Coada este goală.', crossServerEnqueueBlocked: 'Piesele de pe alt server nu pot fi adăugate în coada curentă. Golește sau oprește coada mai întâi.', diff --git a/src/locales/ru/queue.ts b/src/locales/ru/queue.ts index 228b7b9a..34bbf04e 100644 --- a/src/locales/ru/queue.ts +++ b/src/locales/ru/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: 'Закрыть', nextTracks: 'Дальше', shareQueue: 'Копировать ссылку на очередь', + shareNavidromePublic: 'Копировать ссылку Navidrome share', + shareNavidromePublicMissing: 'Ссылка Navidrome share недоступна.', shareQueueEmpty: 'Очередь пуста — нечем поделиться.', emptyQueue: 'Очередь пуста.', crossServerEnqueueBlocked: 'Треки с другого сервера нельзя добавить в текущую очередь. Завершите или очистите очередь.', diff --git a/src/locales/zh/queue.ts b/src/locales/zh/queue.ts index d22022ab..8d383777 100644 --- a/src/locales/zh/queue.ts +++ b/src/locales/zh/queue.ts @@ -33,6 +33,8 @@ export const queue = { close: '关闭', nextTracks: '即将播放', shareQueue: '复制队列分享链接', + shareNavidromePublic: 'Copy Navidrome share link', + shareNavidromePublicMissing: 'Navidrome share link is unavailable.', shareQueueEmpty: '队列为空,无法分享。', emptyQueue: '队列为空。', crossServerEnqueueBlocked: '无法将其他服务器的曲目加入当前播放队列。请先清空或结束当前队列。',