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
+34
View File
@@ -0,0 +1,34 @@
import { useState } from 'react';
import { Orbit as OrbitIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import OrbitStartModal from './OrbitStartModal';
/**
* Topbar trigger — opens the start-session modal. Hidden while a session
* is already active (role !== null) to avoid double-entry.
*/
export default function OrbitStartTrigger() {
const { t } = useTranslation();
const role = useOrbitStore(s => s.role);
const [open, setOpen] = useState(false);
if (role !== null) return null;
return (
<>
<button
type="button"
className="btn btn-surface orbit-start-trigger"
onClick={() => setOpen(true)}
data-tooltip={t('orbit.triggerTooltip')}
data-tooltip-pos="bottom"
style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 1rem' }}
>
<OrbitIcon size={18} />
<span>{t('orbit.triggerLabel')}</span>
</button>
{open && <OrbitStartModal onClose={() => setOpen(false)} />}
</>
);
}