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
+18
View File
@@ -212,6 +212,8 @@ interface PlayerState {
reorderQueue: (startIndex: number, endIndex: number) => void;
removeTrack: (index: number) => void;
shuffleQueue: () => void;
/** Shuffle only the tracks after the current one — leaves played history intact. */
shuffleUpcomingQueue: () => void;
toggleLastfmLove: () => void;
setLastfmLoved: (v: boolean) => void;
@@ -1826,6 +1828,22 @@ export const usePlayerStore = create<PlayerState>()(
syncQueueToServer(result, currentTrack, get().currentTime);
},
shuffleUpcomingQueue: () => {
const { queue, queueIndex, currentTrack } = get();
const upcomingStart = queueIndex + 1;
const upcomingCount = queue.length - upcomingStart;
if (upcomingCount < 2) return;
const head = queue.slice(0, upcomingStart);
const upcoming = queue.slice(upcomingStart);
for (let i = upcoming.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[upcoming[i], upcoming[j]] = [upcoming[j], upcoming[i]];
}
const result = [...head, ...upcoming];
set({ queue: result });
syncQueueToServer(result, currentTrack, get().currentTime);
},
removeTrack: (index) => {
const { queue, queueIndex } = get();
const newQueue = [...queue];