feat(scope): player store scopedBotId + resolveScopedBot helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-05-30 15:09:03 +08:00
1 parent bf1fb1fd88
commit ca07ebc3b7
3 files changed
+62

No files matched your search

+36
View File
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import axios from 'axios'; import axios from 'axios';
import { resolveScopedBot } from './scope.js';
export interface Song { export interface Song {
id: string; id: string;
@@ -50,6 +51,9 @@ export const usePlayerStore = defineStore('player', {
state: () => ({ state: () => ({
bots: [] as BotStatus[], bots: [] as BotStatus[],
activeBotId: null as string | null, activeBotId: null as string | null,
/** When set, the UI is locked to a single bot (dedicated link, from ?bot).
* Source of truth is the URL — never persisted to localStorage. */
scopedBotId: null as string | null,
/** Per-bot queues keyed by botId */ /** Per-bot queues keyed by botId */
queues: {} as Record<string, Song[]>, queues: {} as Record<string, Song[]>,
/** Per-bot timing state keyed by botId */ /** Per-bot timing state keyed by botId */
@@ -73,6 +77,10 @@ export const usePlayerStore = defineStore('player', {
activeBot(): BotStatus | null { activeBot(): BotStatus | null {
return this.bots.find((b) => b.id === this.activeBotId) ?? this.bots[0] ?? null; return this.bots.find((b) => b.id === this.activeBotId) ?? this.bots[0] ?? null;
}, },
/** True when the UI is locked to a single bot via a dedicated link. */
isScoped(): boolean {
return this.scopedBotId !== null;
},
currentSong(): Song | null { currentSong(): Song | null {
return this.activeBot?.currentSong ?? null; return this.activeBot?.currentSong ?? null;
}, },
@@ -125,6 +133,8 @@ export const usePlayerStore = defineStore('player', {
}, },
setActiveBotId(id: string) { setActiveBotId(id: string) {
// While scoped to a dedicated link, switching bots is blocked.
if (this.scopedBotId !== null && id !== this.scopedBotId) return;
this.activeBotId = id; this.activeBotId = id;
// Fetch queue for newly active bot if we don't have it yet // Fetch queue for newly active bot if we don't have it yet
if (!this.queues[id]) { if (!this.queues[id]) {
@@ -132,6 +142,32 @@ export const usePlayerStore = defineStore('player', {
} }
}, },
/** Lock the UI to a single bot (dedicated link). Sets scope first so the
* setActiveBotId guard does not block the switch to the scoped bot. */
setScope(id: string) {
this.scopedBotId = id;
this.activeBotId = id;
// Lazily fetch this bot's queue, mirroring setActiveBotId.
if (!this.queues[id]) {
this.fetchQueue();
}
},
clearScope() {
this.scopedBotId = null;
},
/** Reconcile the scope with the desired id from the URL (?bot). A stale or
* forbidden id resolves to null and clears the scope rather than locking. */
applyScopeFromQuery(requestedId: string | null) {
const r = resolveScopedBot(requestedId, this.bots.map((b) => b.id));
if (r) {
this.setScope(r);
} else if (requestedId) {
this.clearScope();
}
},
updateBotStatus(botId: string, status: BotStatus) { updateBotStatus(botId: string, status: BotStatus) {
const prev = this.bots.find((b) => b.id === botId); const prev = this.bots.find((b) => b.id === botId);
const prevSongId = prev?.currentSong?.id; const prevSongId = prev?.currentSong?.id;
+16
View File
@@ -0,0 +1,16 @@
import { describe, it, expect } from "vitest";
import { resolveScopedBot } from "./scope.js";
describe("resolveScopedBot", () => {
it("returns null when no id requested", () => {
expect(resolveScopedBot(null, ["a", "b"])).toBeNull();
expect(resolveScopedBot(undefined, ["a"])).toBeNull();
expect(resolveScopedBot("", ["a"])).toBeNull();
});
it("returns the id when it exists in the bot list", () => {
expect(resolveScopedBot("b", ["a", "b"])).toBe("b");
});
it("clears (null) when the requested id is not a known bot", () => {
expect(resolveScopedBot("ghost", ["a", "b"])).toBeNull();
});
});
+10
View File
@@ -0,0 +1,10 @@
/** Given the desired scoped id (from ?bot) and the known bot ids, decide the
* effective scope. Returns the id if it exists, else null (graceful clear:
* a stale/forbidden id never locks the UI). */
export function resolveScopedBot(
requestedId: string | null | undefined,
knownBotIds: readonly string[],
): string | null {
if (!requestedId) return null;
return knownBotIds.includes(requestedId) ? requestedId : null;
}