mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(scope): player store scopedBotId + resolveScopedBot helper
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bf1fb1fd88
commit
ca07ebc3b7
3 files changed
+62
No files matched your search
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user