diff --git a/.env.example b/.env.example index 2d60775..7ac7db7 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,11 @@ META_DESCRIPTION=Welcome to DrawPlace! Collaborate with art enthusiasts to creat DRAW_DELAY_MS=5000 DRAW_MAX_POINTS=24 CANVAS_WIDTH=1534 -CANVAS_HEIGHT=1009 \ No newline at end of file +CANVAS_HEIGHT=1009 + +# Danmaku +DANMAKU_ACTIVITY_ID=your_activity +DANMAKU_TOKEN_NAME=your_token_name +DANMAKU_TOKEN=your_custom_token +DANMAKU_ROOT_PATH=https://danmaku.geekpie.club +DANMAKU_SOCKET_PATH=/socket.io diff --git a/components/Board.tsx b/components/Board.tsx index ed62373..199488b 100644 --- a/components/Board.tsx +++ b/components/Board.tsx @@ -30,6 +30,7 @@ import { Button } from "./ui/button"; import { cn } from "@/lib/utils"; import AnnounceModal from "./AnnounceModal"; import { parseInitData } from "@/lib/binary-parser"; +import DanmakuPlayer from "./Danmaku"; const Board = () => { const { config } = useRuntimeConfigContext(); @@ -496,118 +497,120 @@ const Board = () => { {/* Canvas Area - Flexible */} -
- setRatio(e.state.scale)} - doubleClick={{ disabled: true }} - > - -
1 ? "pixelated" : "auto", + +
+ setRatio(e.state.scale)} + doubleClick={{ disabled: true }} + > + +
1 ? "pixelated" : "auto", + }} + > + 0 && editable} + opacity={ + statusConfig.currentViewMode !== ViewMode.MapOnly ? "1" : "0" + } + /> + {settingsConfig.useOverlay && ( + + )} +
+
+
+ setShowLoginModal(false)} + /> + { + updateSettingsConfig({ showGuideOnLoad: false }); + setShowGuideModal(false); + }} + onClose={() => { + updateSettingsConfig({ showGuideOnLoad: false }); + setShowGuideModal(false); + }} + /> + { + updateSettingsConfig({ + announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "", + }); + setShowAnnounce(false); + }} + onClose={() => { + updateSettingsConfig({ + announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "", + }); + setShowAnnounce(false); + }} + /> +
+
- - - setShowLoginModal(false)} - /> - { - updateSettingsConfig({ showGuideOnLoad: false }); - setShowGuideModal(false); - }} - onClose={() => { - updateSettingsConfig({ showGuideOnLoad: false }); - setShowGuideModal(false); - }} - /> - { - updateSettingsConfig({ - announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "", - }); - setShowAnnounce(false); - }} - onClose={() => { - updateSettingsConfig({ - announcementVersion: process.env.NEXT_PUBLIC_APP_VERSION || "", - }); - setShowAnnounce(false); - }} - /> -
- + {editable ? ( + 绘制模式 + ) : ( + 浏览模式 + )} + +
-
+ {/* Bottom Control Area */}
diff --git a/components/Danmaku.tsx b/components/Danmaku.tsx new file mode 100644 index 0000000..31725b2 --- /dev/null +++ b/components/Danmaku.tsx @@ -0,0 +1,167 @@ +"use client"; + +import React, { useEffect, useRef } from 'react'; +import { CommentManager } from '@wiidede/comment-core-library'; +import io, { Socket } from 'socket.io-client'; +import 'comment-core-library/dist/css/style.css'; +import { useSettingsConfigContext } from "./SettingsProvider"; +import { useRuntimeConfigContext } from "./RuntimeConfigProvider"; +import { toast } from 'sonner'; + +export interface RawDanmakuData { + id?: number; + text: string; + mode: number; + size: number; + color: number; + time: number; + dur?: number; + addons?: Record; +} + +interface DanmakuPlayerProps { + children: React.ReactNode; + activityId?: string; + tokenName?: string; + token?: string; + rootPath?: string; + socketPath?: string; +} + +const DanmakuPlayer = ({ + children, +}: { children: React.ReactNode }) => { + const stageRef = useRef(null); + const cmRef = useRef(null); + const socketRef = useRef(null); + const { config, updateStatus } = useSettingsConfigContext(); + const { config: runtimeConfig } = useRuntimeConfigContext(); + + const activityId = runtimeConfig.DANMAKU_ACTIVITY_ID; + const tokenName = runtimeConfig.DANMAKU_TOKEN_NAME; + const token = runtimeConfig.DANMAKU_TOKEN; + const rootPath = runtimeConfig.DANMAKU_ROOT_PATH; + const socketPath = runtimeConfig.DANMAKU_SOCKET_PATH; + + useEffect(() => { + if (!stageRef.current) return; + + const cm = new CommentManager(stageRef.current); + cm.init(); + cm.start(); + + // 动态调整弹幕缩放比例,基于屏幕宽度 + // cm.options.scroll.scale = Math.round(document.body.clientWidth / 500); + + cm.setBounds(); + cmRef.current = cm; + + const handleResize = () => cm.setBounds(); + window.addEventListener('resize', handleResize); + + return () => { + cm.stop(); + window.removeEventListener('resize', handleResize); + }; + }, []); + + // Socket connection + useEffect(() => { + if (!activityId || !rootPath) { + return; + } + + // 注意:Namespace 是 /danmaku + const socket = io(`${rootPath}/danmaku`, { + path: socketPath, + query: { + activity: activityId, + tokenName: tokenName, + token: token, + }, + transports: ['websocket', 'polling'] + }); + + socket.on('connect', () => { + console.log(`已连接到弹幕服务器 (Activity: ${activityId})`); + // toast.info("已连接到弹幕服务器"); + updateStatus({ isDanmakuConnected: true }); + }); + + socket.on('disconnect', () => { + console.log('与弹幕服务器断开连接'); + toast.warning("与弹幕服务器断开连接"); + updateStatus({ isDanmakuConnected: false }); + }); + + socket.on('danmaku', (data: RawDanmakuData) => { + console.log('收到弹幕:', data); + if (cmRef.current) { + cmRef.current.send({ + text: data.text, + mode: data.mode, + size: data.size, + color: data.color, + dur: data.dur * 2, + stime: data.time, + shadow: data.color === 0xffffff, + ...data.addons + }); + } + }); + + socketRef.current = socket; + + return () => { + if (socketRef.current) { + socketRef.current.disconnect(); + updateStatus({ isDanmakuConnected: false }); + } + }; + }, [activityId, tokenName, token, rootPath, socketPath]); + + const sendTestComment = () => { + if (cmRef.current) { + cmRef.current.send({ + text: "这是一条测试弹幕 " + Math.random().toFixed(2), + mode: 1, // 滚动模式 + size: 25, + color: 0xffffff, + dur: 4000 + }); + } + }; + + return ( +
+
+ {children} + {/* */} +
+ ); +}; + +export default DanmakuPlayer; diff --git a/components/Dock.tsx b/components/Dock.tsx index afd86ed..7738779 100644 --- a/components/Dock.tsx +++ b/components/Dock.tsx @@ -12,6 +12,8 @@ import { PaintRoller, Palette, Settings, + MessageSquare, + MessageSquareOff, } from "lucide-react"; import { Input } from "./ui/input"; import { @@ -50,8 +52,12 @@ const Dock = ({ handleDraw, }) => { const { config } = useRuntimeConfigContext(); - const { status: statusConfig, updateStatus: updateStatusConfig } = - useSettingsConfigContext(); + const { + config: settingsConfig, + updateConfig: updateSettingsConfig, + status: statusConfig, + updateStatus: updateStatusConfig, + } = useSettingsConfigContext(); if (!dataSource) return null; const [token, setToken] = useState(""); const [isLoggedIn, setIsLoggedIn] = useState(false); @@ -142,6 +148,25 @@ const Dock = ({ )} + +