exp(orbit): host merge, guest queue view, settings popover, i18n

Guest suggestions now auto-merge into the host's play queue at random
positions inside the upcoming range, so they actually surface alongside
host-picked tracks instead of piling up at the end. Per-item dedupe via
addedBy:addedAt:trackId keys survives reshuffles.

Guests get a read-only mirror of the session in place of their queue
panel — live-card for the host's current track, suggestion list with
submitter attribution, and a footer reminding them the host owns
playback. Track metadata is resolved lazily and cached locally.

New host-only settings popover (gear in the bar) with auto-approve and
auto-shuffle toggles plus a "Shuffle now" action. Settings live in the
OrbitState blob and push immediately to Navidrome; missing fields on
older blobs default to "both on".

15-min shuffle now touches the real playerStore queue via a new
shuffleUpcomingQueue action — previous behaviour only reshuffled the
guest-facing suggestion history. A manual shuffle is available from the
settings popover so the interval can be verified without waiting.

Start-modal reworked into a single step: share-link is visible and
copy-able from the start, auto-copied on Start if the host hasn't
already hit Copy. Link carries a live name-derived slug (parser strips
it, SID is authoritative). LAN/remote-server hint above the facts.
Random session-name suggester pulls from three pattern pools for ~10k
unique combinations.

All user-visible Orbit strings moved to a dedicated orbit i18n
namespace (en + de); other locales fall back to en via i18next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-23 02:49:42 +02:00
parent 7fe492d233
commit 2b97a7b01e
19 changed files with 1647 additions and 213 deletions
+82
View File
@@ -1435,4 +1435,86 @@ export const deTranslation = {
liveSearch: 'Live',
randomAlbumsLabel: 'Zufallsalben',
},
orbit: {
triggerLabel: 'Psy Orbit',
triggerTooltip: 'Gemeinsame Hör-Session starten',
closeAria: 'Schließen',
heroTitlePrefix: 'Gemeinsam hören mit',
heroTitleBrand: 'Psy Orbit',
heroSub: 'Starte eine Session — andere Nutzer auf {{server}} hören im Takt mit dir und können Songs vorschlagen.',
fallbackServer: 'diesem Server',
tipLan: 'Du bist aktuell mit einer Heimnetz-Adresse verbunden. Gäste außerhalb deines Heimnetzes können so nicht beitreten — wechsle vor dem Start in den Servereinstellungen zu einer öffentlichen Adresse.',
tipRemote: 'Damit Gäste außerhalb deines Heimnetzes beitreten können, muss deine Serveradresse öffentlich erreichbar sein. Ist dein Server nur intern verfügbar, wechsle vor dem Start zu einer öffentlichen Adresse.',
factSameServer: 'Nur angemeldete Nutzer desselben Servers können beitreten.',
factHost: 'Du bist Gastgeber: du steuerst die Wiedergabe, deine Gäste schlagen Songs vor.',
factShuffle: 'Die Warteschlange wird automatisch alle 15 Minuten neu gemischt.',
labelName: 'Sessionname',
namePlaceholder: 'Velvet Orbit',
reshuffleTooltip: 'Neuer Vorschlag',
reshuffleAria: 'Neuen Namen vorschlagen',
helperName: 'So taucht die Session für deine Gäste auf — Vorschlag übernehmen oder eigenen eintippen.',
labelMax: 'Maximale Gäste',
helperMax: 'Du zählst nicht mit — das ist nur die Obergrenze für beitretende Gäste.',
labelLink: 'Einladungslink',
tooltipCopied: 'Kopiert',
tooltipCopy: 'Kopieren',
ariaCopyLink: 'Link kopieren',
helperLink: 'Schicke diesen Link deinen Gästen. Sie fügen ihn mit Strg+V irgendwo in Psysonic ein.',
btnCancel: 'Abbrechen',
btnStarting: 'Starte…',
btnStart: 'Orbit starten',
btnCopyAndStart: 'Link kopieren & starten',
errNameRequired: 'Bitte gib deiner Session einen Namen.',
errStartFailed: 'Konnte nicht starten.',
hostLabel: 'Gastgeber: {{name}}',
participantsTooltip: 'Teilnehmer',
settingsTooltip: 'Session-Einstellungen',
shuffleLabel: 'Warteschlange wird neu gemischt in',
catchUpLabel: 'aufholen',
catchUpTooltip: 'Zur aktuellen Host-Position springen',
endTooltip: 'Session beenden',
leaveTooltip: 'Session verlassen',
participantsInviteLabel: 'Einladungslink',
participantsCountLabel: '{{count}} in Session',
participantsHost: 'Gastgeber',
participantsEmpty: 'Noch keine Gäste',
participantsKickTooltip: 'Aus Session entfernen',
participantsKickAria: '{{user}} entfernen',
settingsTitle: 'Session-Einstellungen',
settingAutoApprove: 'Vorschläge automatisch übernehmen',
settingAutoApproveHint: 'Neue Gäste-Vorschläge landen sofort in deiner Warteschlange. Aus: sie bleiben in der Session-Liste, du musst sie später manuell übernehmen.',
settingAutoShuffle: 'Warteschlange alle 15 Minuten mischen',
settingAutoShuffleHint: 'Automatischer Fisher-Yates-Shuffle. Aus: Reihenfolge wird nur verändert, wenn du selbst sortierst.',
settingShuffleNow: 'Jetzt mischen',
toastSuggested: '{{user}} hat „{{title}}" vorgeschlagen',
toastSuggestedMany: '{{count}} neue Vorschläge in der Warteschlange',
toastShuffled: 'Warteschlange gemischt',
toastJoined: 'Session beigetreten',
toastLoginFirst: 'Erst anmelden, um einer Session beizutreten',
toastSwitchServer: 'Wechsle erst zu {{url}} und füge den Link dann erneut ein',
toastJoinFail: 'Session konnte nicht beigetreten werden',
joinErrNotFound: 'Session nicht gefunden',
joinErrEnded: 'Session wurde beendet',
joinErrFull: 'Session ist voll',
joinErrKicked: 'Du kannst dieser Session nicht mehr beitreten',
joinErrNoUser: 'Kein aktiver Server',
joinErrServerError: 'Beitritt fehlgeschlagen',
ctxAddToSession: 'Zur Orbit-Session hinzufügen',
ctxSuggestedToast: 'An Session vorgeschlagen',
ctxSuggestFailed: 'Vorschlag fehlgeschlagen — nicht beigetreten',
guestLive: 'Live',
guestPlaying: 'Läuft gerade',
guestPaused: 'Pausiert',
guestLoading: 'Lade…',
guestSuggestions: 'Vorschläge',
guestHost: 'Gastgeber: {{name}}',
guestSubmitter: 'von {{user}}',
guestEmpty: 'Noch keine Vorschläge. Öffne bei einem Song das Kontextmenü und wähle „Zur Orbit-Session hinzufügen".',
guestFooter: 'Die Wiedergabe wird vom Gastgeber gesteuert — du kannst die Liste nicht ändern.',
exitKickedTitle: 'Du wurdest aus der Session entfernt',
exitEndedTitle: 'Der Gastgeber hat die Session beendet',
exitKickedBody: '{{host}} hat dich aus „{{name}}" entfernt.',
exitEndedBody: '„{{name}}" ist zu Ende. Hoffentlich war\'s schön.',
exitOk: 'OK',
},
};