feat(share): library deep links (psysonic2) with paste + toolbar actions (#261)

* feat(share): library deep links (psysonic2) with paste and toolbar actions

Add base64url-encoded payloads for track, album, artist, and ordered queue.
Handle paste outside inputs to switch server, validate entities, navigate or
play. Reuse clipboard helper for context menu, queue panel, album and artist
headers. Tests distinguish server invite strings from library shares.

* fix(share): play partial queue when some shared tracks are missing

Skip unavailable song IDs when pasting a queue share while preserving order.
Toast when some tracks are missing; error only if none resolve. Add
openedQueuePartial and queueAllUnavailable i18n; remove queueTracksMissing.

* feat(settings): paste server invites globally and scroll to add form

Handle psysonic1- magic strings outside inputs: navigate to settings or login
with prefilled add-server fields. Decode invites embedded in surrounding text.
Scroll the add-server block into view when opening from a paste so long server
lists stay oriented.

---------

Co-authored-by: Maxim Isaev <im@friclub.ru>
This commit is contained in:
Frank Stellmacher
2026-04-22 11:21:00 +02:00
committed by GitHub
parent 21d00889aa
commit 7c9a300022
22 changed files with 779 additions and 18 deletions
+25
View File
@@ -126,10 +126,32 @@ export const ruTranslation = {
selectedAlbums: '{{count}} альбомов выбрано',
selectedArtists: '{{count}} исполнителей выбрано',
songInfo: 'Сведения о треке',
shareLink: 'Копировать ссылку для обмена',
shareCopied: 'Ссылка для обмена скопирована в буфер.',
shareCopyFailed: 'Не удалось скопировать в буфер.',
},
sharePaste: {
notLoggedIn: 'Войдите и добавьте сервер, затем вставьте ссылку.',
noMatchingServer: 'Ни один сохранённый сервер не совпадает с адресом из ссылки. Добавьте сервер: {{url}}',
trackUnavailable: 'Трек на сервере не найден.',
albumUnavailable: 'Альбом на сервере не найден.',
artistUnavailable: 'Исполнитель на сервере не найден.',
openedTrack: 'Воспроизводится присланный трек.',
openedAlbum: 'Открывается присланный альбом.',
openedArtist: 'Открывается присланный исполнитель.',
openedQueue_one: 'Воспроизводится присланная очередь: {{count}} трек.',
openedQueue_few: 'Воспроизводится присланная очередь: {{count}} трека.',
openedQueue_many: 'Воспроизводится присланная очередь: {{count}} треков.',
openedQueue_other: 'Воспроизводится присланная очередь: {{count}} треков.',
openedQueuePartial:
'Воспроизводится {{played}} из {{total}} треков по ссылке ({{skipped}} на этом сервере не найдены).',
queueAllUnavailable: 'Ни один из треков по ссылке не найден на сервере.',
genericError: 'Не удалось открыть ссылку для обмена.',
},
albumDetail: {
back: 'Назад',
playAll: 'Воспроизвести всё',
shareAlbum: 'Поделиться альбомом',
enqueue: 'В очередь',
enqueueTooltip: 'Добавить весь альбом в очередь',
artistBio: 'Биография',
@@ -185,6 +207,7 @@ export const ruTranslation = {
albums: 'Альбомы',
album: 'Альбом',
playAll: 'Воспроизвести всё',
shareArtist: 'Поделиться исполнителем',
shuffle: 'Перемешать',
radio: 'Радио',
loading: 'Загрузка…',
@@ -1040,6 +1063,8 @@ export const ruTranslation = {
hide: 'Скрыть',
close: 'Закрыть',
nextTracks: 'Дальше',
shareQueue: 'Копировать ссылку на очередь',
shareQueueEmpty: 'Очередь пуста — нечем поделиться.',
emptyQueue: 'Очередь пуста.',
trackSingular: 'трек',
trackPlural: 'треков',