- Add IDanmakuService interface and DanmakuService implementation to parse danmaku XML files
- Add GET /api/record-tasks/{id}/danmaku and GET /api/record-sessions/{id}/danmaku endpoints
- Add DanmakuPlayer Vue component with native video + CSS overlay danmaku rendering
- Add danmakuEngine.ts pure-TypeScript animation loop with binary search, track management, and event notifications
- Add useDanmakuPlayer composable for reusable danmaku data loading
- Integrate danmaku toggle button into RecordTaskDetailView
- Integrate danmaku replay modal dialog into RecordSessionDetailView segment table
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import { ref } from "vue";
|
|
import apiClient, { getApiErrorMessage } from "@/api/client";
|
|
import type { DanmakuEvent, DanmakuResponse, SessionDanmakuResponse } from "@/types";
|
|
|
|
export function useDanmakuPlayer() {
|
|
const danmakuEvents = ref<DanmakuEvent[]>([]);
|
|
const loading = ref(false);
|
|
const error = ref("");
|
|
|
|
async function loadTaskDanmaku(taskId: string): Promise<boolean> {
|
|
loading.value = true;
|
|
error.value = "";
|
|
|
|
try {
|
|
const { data } = await apiClient.get<DanmakuResponse>(`/record-tasks/${taskId}/danmaku`);
|
|
danmakuEvents.value = data.events;
|
|
return data.events.length > 0;
|
|
} catch (err) {
|
|
if ((err as { response?: { status?: number } })?.response?.status === 404) {
|
|
error.value = "该分片没有弹幕数据。";
|
|
} else {
|
|
error.value = getApiErrorMessage(err, "弹幕数据加载失败。");
|
|
}
|
|
danmakuEvents.value = [];
|
|
return false;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadSessionDanmaku(sessionId: string): Promise<boolean> {
|
|
loading.value = true;
|
|
error.value = "";
|
|
|
|
try {
|
|
const { data } = await apiClient.get<SessionDanmakuResponse>(`/record-sessions/${sessionId}/danmaku`);
|
|
// Flatten all task events into a single list with session-level offsets
|
|
const allEvents: DanmakuEvent[] = [];
|
|
for (const task of data.tasks) {
|
|
allEvents.push(...task.events);
|
|
}
|
|
// Sort by offset for proper playback order
|
|
allEvents.sort((a, b) => a.offsetSeconds - b.offsetSeconds);
|
|
danmakuEvents.value = allEvents;
|
|
return allEvents.length > 0;
|
|
} catch (err) {
|
|
if ((err as { response?: { status?: number } })?.response?.status === 404) {
|
|
error.value = "该场次没有弹幕数据。";
|
|
} else {
|
|
error.value = getApiErrorMessage(err, "弹幕数据加载失败。");
|
|
}
|
|
danmakuEvents.value = [];
|
|
return false;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function clear(): void {
|
|
danmakuEvents.value = [];
|
|
error.value = "";
|
|
loading.value = false;
|
|
}
|
|
|
|
return { danmakuEvents, loading, error, loadTaskDanmaku, loadSessionDanmaku, clear };
|
|
}
|