0.9.9
parent
155eb06f8a
commit
5ae442c4b2
|
|
@ -1,114 +0,0 @@
|
||||||
import { useEffect, useState, useRef } from 'react';
|
|
||||||
import { request } from '../utils/request';
|
|
||||||
|
|
||||||
interface DanmakuMessage {
|
|
||||||
id: string;
|
|
||||||
uid: string;
|
|
||||||
username: string;
|
|
||||||
text: string;
|
|
||||||
ts: number;
|
|
||||||
// Runtime properties for animation
|
|
||||||
top?: number;
|
|
||||||
duration?: number;
|
|
||||||
startTime?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DanmakuLayerProps {
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DanmakuLayer({ enabled }: DanmakuLayerProps) {
|
|
||||||
const [visibleMessages, setVisibleMessages] = useState<DanmakuMessage[]>([]);
|
|
||||||
const processedIds = useRef<Set<string>>(new Set());
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Polling for new messages
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
|
|
||||||
const fetchDanmaku = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await request.get('/danmaku/list');
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
// Filter out messages we've already seen or processed locally
|
|
||||||
// Actually, for a "live" feel, we only want *recent* messages or messages we haven't shown in this session.
|
|
||||||
// But since the backend returns a 24h window, we don't want to replay all 24h history at once on load.
|
|
||||||
// Strategy: On first load, maybe only show last 20? Or just start listening for new ones?
|
|
||||||
// Let's show recent ones (last 1 min) on load, then polling.
|
|
||||||
|
|
||||||
const now = Date.now() / 1000;
|
|
||||||
const newMessages = data.filter((msg: DanmakuMessage) => {
|
|
||||||
if (processedIds.current.has(msg.id)) return false;
|
|
||||||
// Only show messages from the last 5 minutes to avoid flooding history on reload
|
|
||||||
if (now - msg.ts > 300) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newMessages.length > 0) {
|
|
||||||
console.log(`[Danmaku] Received ${newMessages.length} new messages`, newMessages);
|
|
||||||
newMessages.forEach((msg: DanmakuMessage) => processedIds.current.add(msg.id));
|
|
||||||
// Add to queue
|
|
||||||
addMessagesToTrack(newMessages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to fetch danmaku", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDanmaku(); // Initial fetch
|
|
||||||
const interval = setInterval(fetchDanmaku, 3000); // Poll every 3s
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [enabled]);
|
|
||||||
|
|
||||||
const addMessagesToTrack = (newMsgs: DanmakuMessage[]) => {
|
|
||||||
// Assign random vertical position and duration
|
|
||||||
const tracks = newMsgs.map(msg => ({
|
|
||||||
...msg,
|
|
||||||
top: Math.floor(Math.random() * 60) + 10, // 10% to 70% height
|
|
||||||
duration: Math.floor(Math.random() * 5) + 8, // 8-13 seconds duration
|
|
||||||
startTime: Date.now()
|
|
||||||
}));
|
|
||||||
|
|
||||||
setVisibleMessages(prev => [...prev, ...tracks]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cleanup finished animations
|
|
||||||
const handleAnimationEnd = (id: string) => {
|
|
||||||
setVisibleMessages(prev => prev.filter(m => m.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!enabled) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="absolute inset-0 pointer-events-none z-50 overflow-hidden"
|
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
>
|
|
||||||
{visibleMessages.map(msg => (
|
|
||||||
<div
|
|
||||||
key={msg.id}
|
|
||||||
className="absolute whitespace-nowrap text-white font-bold text-shadow-md will-change-transform"
|
|
||||||
style={{
|
|
||||||
top: `${msg.top}%`,
|
|
||||||
left: '100%',
|
|
||||||
fontSize: '1.5rem', // Increased size for visibility
|
|
||||||
textShadow: '0 0 4px rgba(0,0,0,0.8), 0 0 2px rgba(0,0,0,1)', // Stronger shadow
|
|
||||||
animation: `danmaku-move ${msg.duration}s linear forwards`
|
|
||||||
}}
|
|
||||||
onAnimationEnd={() => handleAnimationEnd(msg.id)}
|
|
||||||
>
|
|
||||||
{msg.text}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<style>{`
|
|
||||||
@keyframes danmaku-move {
|
|
||||||
0% { transform: translateX(0); }
|
|
||||||
100% { transform: translateX(-100vw - 100%); }
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -41,9 +41,15 @@ export function MessageBoard({ open, onClose }: MessageBoardProps) {
|
||||||
fetchMessages().finally(() => setLoading(false));
|
fetchMessages().finally(() => setLoading(false));
|
||||||
intervalRef.current = window.setInterval(fetchMessages, 3000);
|
intervalRef.current = window.setInterval(fetchMessages, 3000);
|
||||||
} else {
|
} else {
|
||||||
clearInterval(intervalRef.current);
|
if (intervalRef.current !== null) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return () => clearInterval(intervalRef.current);
|
return () => {
|
||||||
|
if (intervalRef.current !== null) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ export function TerminalModal({
|
||||||
}
|
}
|
||||||
.terminal-modal .ant-modal-close {
|
.terminal-modal .ant-modal-close {
|
||||||
color: #2ea043 !important;
|
color: #2ea043 !important;
|
||||||
top: 12px !important;
|
|
||||||
}
|
}
|
||||||
.terminal-modal .ant-modal-close:hover {
|
.terminal-modal .ant-modal-close:hover {
|
||||||
background-color: rgba(35, 134, 54, 0.2) !important;
|
background-color: rgba(35, 134, 54, 0.2) !important;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue