feat: 支持设置 token
This commit is contained in:
parent
a3814b8adf
commit
25b7dd0f82
188
app/danmu/controls.tsx
Normal file
188
app/danmu/controls.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
"use client";
|
||||||
|
import { useRef, useState, useEffect } from "react";
|
||||||
|
import { query } from "@/lib/actions/lyrics";
|
||||||
|
|
||||||
|
const useLyricsRunner = (runner: (lyric: string) => void, interval: number) => {
|
||||||
|
const [lyrics, setLyrics] = useState<string[]>([]);
|
||||||
|
const [offset, setOffset] = useState<number>(0);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const startTime = useRef<number>(Date.now());
|
||||||
|
const ref = useRef<{ lyrics: string[]; offset: number }>({ lyrics, offset });
|
||||||
|
|
||||||
|
const clearTimer = (): void => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doLyric = (): void => {
|
||||||
|
console.log("testing ...", ref.current.lyrics.length);
|
||||||
|
if (ref.current.lyrics.length > 0) {
|
||||||
|
const currentTime = Date.now() - startTime.current;
|
||||||
|
const found = ref.current.lyrics.find((lyric) => {
|
||||||
|
const m = Number(lyric.slice(1, 3));
|
||||||
|
const s = Number(lyric.slice(4, 6));
|
||||||
|
const ms10 = Number(lyric.slice(7, 9));
|
||||||
|
const ms = (m * 60 + s) * 1000 + ms10 * 10;
|
||||||
|
return (
|
||||||
|
currentTime > ms + ref.current.offset &&
|
||||||
|
ms + ref.current.offset > currentTime - interval
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (found) {
|
||||||
|
runner(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current.offset = offset;
|
||||||
|
ref.current.lyrics = lyrics;
|
||||||
|
}, [offset, lyrics]);
|
||||||
|
|
||||||
|
const start = (): void => {
|
||||||
|
clearTimer();
|
||||||
|
startTime.current = Date.now();
|
||||||
|
timerRef.current = setInterval(doLyric, interval);
|
||||||
|
};
|
||||||
|
const pause = (): void => {
|
||||||
|
clearTimer();
|
||||||
|
};
|
||||||
|
const resume = (): void => {
|
||||||
|
clearTimer();
|
||||||
|
timerRef.current = setInterval(doLyric, interval);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
offset,
|
||||||
|
setOffset,
|
||||||
|
setLyrics,
|
||||||
|
resume,
|
||||||
|
start,
|
||||||
|
pause,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LyricItem {
|
||||||
|
timestamp: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Controls(): JSX.Element {
|
||||||
|
const ref = useRef<HTMLDialogElement | null>(null);
|
||||||
|
const [history, setHistory] = useState<LyricItem[]>([]);
|
||||||
|
|
||||||
|
const { start, resume, pause, setLyrics, offset, setOffset } =
|
||||||
|
useLyricsRunner((line) => {
|
||||||
|
setHistory((items) => [
|
||||||
|
{
|
||||||
|
content: line.slice(0, 10),
|
||||||
|
timestamp: line.slice(10),
|
||||||
|
},
|
||||||
|
...items,
|
||||||
|
]);
|
||||||
|
console.log(line);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center p-4 border-t border-base-300">
|
||||||
|
<div className="columns-1">
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="join">
|
||||||
|
<button
|
||||||
|
className="btn join-item"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
pause();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
暂停输出
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn join-item"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
resume();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
继续输出
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn join-item"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOffset(offset + 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+1秒
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn join-item"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOffset(offset - 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
-1秒
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn join-item"
|
||||||
|
onClick={() => {
|
||||||
|
query({
|
||||||
|
name: "红豆",
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// eslint-disable-next-line no-console -- temp
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
setLyrics(result.lyrics.split("\n"));
|
||||||
|
setHistory([]);
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// eslint-disable-next-line no-console -- temp
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
开始
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="w-full mt-1 pt-0 pb-1 rounded-xl bg-gray-100 border-t-gray-100"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div className="text-center">当前延迟 {offset / 1000} 秒</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-h-40 overflow-auto">
|
||||||
|
<ul className="timeline timeline-vertical">
|
||||||
|
{history.map((item, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<div className="timeline-start">{item.timestamp}</div>
|
||||||
|
<div className="timeline-middle">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="timeline-end timeline-box">{item.content}</div>
|
||||||
|
<hr />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
71
app/danmu/danmu.tsx
Normal file
71
app/danmu/danmu.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||||
|
import MessageDispatcher from "./message";
|
||||||
|
import { Token } from "@/lib/types";
|
||||||
|
|
||||||
|
const MOMO_IM_URL = "wss://live-ws.immomo.com/ws/im";
|
||||||
|
|
||||||
|
export const Danmu: React.FC<{ token: Token }> = ({ token }) => {
|
||||||
|
const [messageHistory, setMessageHistory] = useState<MessageEvent<any>[]>([]);
|
||||||
|
|
||||||
|
const { sendMessage, lastMessage, readyState } = useWebSocket(MOMO_IM_URL, {
|
||||||
|
heartbeat: {
|
||||||
|
message: () => {
|
||||||
|
const pingMessage = {
|
||||||
|
msg_id: 2,
|
||||||
|
client_time: Date.now(),
|
||||||
|
type: "Ping",
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
return JSON.stringify(pingMessage);
|
||||||
|
},
|
||||||
|
interval: 5000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const momoId = token.uid;
|
||||||
|
const roomId = "1476810196216";
|
||||||
|
const momoToken = token.token;
|
||||||
|
|
||||||
|
const clientTime = Date.now();
|
||||||
|
const data = {
|
||||||
|
msg_id: 1,
|
||||||
|
client_time: clientTime,
|
||||||
|
type: "Sauth",
|
||||||
|
data: {
|
||||||
|
momoid: momoId,
|
||||||
|
roomid: roomId,
|
||||||
|
role: 6,
|
||||||
|
isVisitor: false,
|
||||||
|
token: momoToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ReadyState.OPEN === readyState) {
|
||||||
|
sendMessage(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}, [readyState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastMessage !== null) {
|
||||||
|
if (lastMessage.data) {
|
||||||
|
const message = JSON.parse(lastMessage.data);
|
||||||
|
if (message.type === "pong") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(message);
|
||||||
|
setMessageHistory((prev) => prev.concat(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [lastMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[50vh] overflow-auto">
|
||||||
|
{messageHistory.map((message, idx) => (
|
||||||
|
<MessageDispatcher key={idx} message={message} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
21
app/danmu/message.tsx
Normal file
21
app/danmu/message.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default function MessageDispatcher({ message }: { message: any }) {
|
||||||
|
const messageType = message?.groups?.[0]?.units?.[0]?.type;
|
||||||
|
let nick = "wait";
|
||||||
|
let content = "wait";
|
||||||
|
|
||||||
|
if (message?.type === 'sauth_ret') {
|
||||||
|
return <div>{message?.data?.em}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageType === "Message") {
|
||||||
|
nick = message?.groups?.[0]?.nick;
|
||||||
|
content = message?.groups?.[0]?.units?.[0]?.["Msg.Message.data"]?.text;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="chat chat-start">
|
||||||
|
<div className="chat-bubble">
|
||||||
|
{nick}: {content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,191 +1,30 @@
|
|||||||
/* eslint-disable no-console -- debug */
|
import { getToken } from "@/lib/actions/token";
|
||||||
"use client";
|
import Controls from "./controls";
|
||||||
import { useRef, useState, useEffect } from "react";
|
import { Danmu } from "./danmu";
|
||||||
import { query } from "@/lib/actions/lyrics";
|
|
||||||
|
|
||||||
const useLyricsRunner = (runner: (lyric: string) => void, interval: number) => {
|
|
||||||
const [lyrics, setLyrics] = useState<string[]>([]);
|
|
||||||
const [offset, setOffset] = useState<number>(0);
|
|
||||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
||||||
const startTime = useRef<number>(Date.now());
|
|
||||||
const ref = useRef<{ lyrics: string[]; offset: number }>({ lyrics, offset });
|
|
||||||
|
|
||||||
const clearTimer = (): void => {
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearInterval(timerRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const doLyric = (): void => {
|
|
||||||
console.log("testing ...", ref.current.lyrics.length);
|
|
||||||
if (ref.current.lyrics.length > 0) {
|
|
||||||
const currentTime = Date.now() - startTime.current;
|
|
||||||
const found = ref.current.lyrics.find((lyric) => {
|
|
||||||
const m = Number(lyric.slice(1, 3));
|
|
||||||
const s = Number(lyric.slice(4, 6));
|
|
||||||
const ms10 = Number(lyric.slice(7, 9));
|
|
||||||
const ms = (m * 60 + s) * 1000 + ms10 * 10;
|
|
||||||
return (
|
|
||||||
currentTime > ms + ref.current.offset &&
|
|
||||||
ms + ref.current.offset > currentTime - interval
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (found) {
|
|
||||||
runner(found);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current.offset = offset;
|
|
||||||
ref.current.lyrics = lyrics;
|
|
||||||
}, [offset, lyrics]);
|
|
||||||
|
|
||||||
const start = (): void => {
|
|
||||||
clearTimer();
|
|
||||||
startTime.current = Date.now();
|
|
||||||
timerRef.current = setInterval(doLyric, interval);
|
|
||||||
};
|
|
||||||
const pause = (): void => {
|
|
||||||
clearTimer();
|
|
||||||
};
|
|
||||||
const resume = (): void => {
|
|
||||||
clearTimer();
|
|
||||||
timerRef.current = setInterval(doLyric, interval);
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
setOffset,
|
|
||||||
setLyrics,
|
|
||||||
resume,
|
|
||||||
start,
|
|
||||||
pause,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LyricItem {
|
|
||||||
timestamp: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page(): JSX.Element {
|
|
||||||
const ref = useRef<HTMLDialogElement | null>(null);
|
|
||||||
const [history, setHistory] = useState<LyricItem[]>([]);
|
|
||||||
|
|
||||||
const { start, resume, pause, setLyrics, offset, setOffset } =
|
|
||||||
useLyricsRunner((line) => {
|
|
||||||
setHistory((items) => [
|
|
||||||
{
|
|
||||||
content: line.slice(0, 10),
|
|
||||||
timestamp: line.slice(10),
|
|
||||||
},
|
|
||||||
...items,
|
|
||||||
]);
|
|
||||||
console.log(line);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const token = await getToken();
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row">
|
||||||
|
<section className="basis-1/3 card">
|
||||||
|
<div className="card-body max-h-[50vh] overflow-auto">直播间列表</div>
|
||||||
|
</section>
|
||||||
|
<section className="basis-1/3 card">
|
||||||
|
<div className="card-body max-h-[50vh] overflow-auto">歌曲清单</div>
|
||||||
|
</section>
|
||||||
|
<section className="basis-1/3 card">
|
||||||
|
<div className="card-body">
|
||||||
<div className="mockup-browser border">
|
<div className="mockup-browser border">
|
||||||
<div className="mockup-browser-toolbar">
|
<div className="mockup-browser-toolbar">
|
||||||
<div className="input">红豆</div>
|
<div className="input">红豆</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center px-4 py-16 border-t border-base-300">
|
<div className="divide-y divide-gray-300">
|
||||||
<div className="columns-1">
|
<Controls />
|
||||||
<div className="w-full">
|
<Danmu token={token} />
|
||||||
<div className="join">
|
|
||||||
<div className="stat join-item">延迟 {offset / 1000} 秒</div>
|
|
||||||
<button
|
|
||||||
className="btn join-item"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
pause();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
暂停输出
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn join-item"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
resume();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
继续输出
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn join-item"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setOffset(offset + 1000);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+1秒
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn join-item"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setOffset(offset - 1000);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
-1秒
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn join-item"
|
|
||||||
onClick={() => {
|
|
||||||
query({
|
|
||||||
name: "红豆",
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
// eslint-disable-next-line no-console -- temp
|
|
||||||
console.log(result);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
setLyrics(result.lyrics.split("\n"));
|
|
||||||
setHistory([]);
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// eslint-disable-next-line no-console -- temp
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
开始
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full mt-8">
|
|
||||||
<ul className="timeline timeline-vertical">
|
|
||||||
{history.map((item, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
<div className="timeline-start">{item.timestamp}</div>
|
|
||||||
<div className="timeline-middle">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="timeline-end timeline-box">
|
|
||||||
{item.content}
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import Logo from "@/components/logo";
|
|||||||
import BackButton from "@/components/back-button";
|
import BackButton from "@/components/back-button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AdminButton } from "@/components/admin-button";
|
import { AdminButton } from "@/components/admin-button";
|
||||||
|
import TokenButton from "@/components/token-button";
|
||||||
|
import { getToken } from "@/lib/actions/token";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
@ -14,11 +16,12 @@ export const metadata: Metadata = {
|
|||||||
description: "搜索歌曲,选择直播间,智能发送弹幕",
|
description: "搜索歌曲,选择直播间,智能发送弹幕",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const token = await getToken();
|
||||||
return (
|
return (
|
||||||
<html lang="zh_CN" data-theme="light">
|
<html lang="zh_CN" data-theme="light">
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
@ -38,7 +41,7 @@ export default function RootLayout({
|
|||||||
<Link href="/songs" className="mr-5 hover:text-gray-900">
|
<Link href="/songs" className="mr-5 hover:text-gray-900">
|
||||||
歌词曲库
|
歌词曲库
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/live" className="mr-5 hover:text-gray-900">
|
<Link target="_blank" href="https://tachy.daoyoucloud.com/apps/danmu-sim/admin/9nzr816par6" className="mr-5 hover:text-gray-900">
|
||||||
直播间列表
|
直播间列表
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/danmu" className="mr-5 hover:text-gray-900">
|
<Link href="/danmu" className="mr-5 hover:text-gray-900">
|
||||||
@ -49,8 +52,8 @@ export default function RootLayout({
|
|||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="join">
|
<div className="join">
|
||||||
<BackButton className="btn join-item" />
|
|
||||||
<AdminButton className="btn join-item" />
|
<AdminButton className="btn join-item" />
|
||||||
|
<TokenButton className="btn join-item" token={token} />
|
||||||
<SignoutButton className="btn join-item" />
|
<SignoutButton className="btn join-item" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
|
||||||
|
|
||||||
const MOMO_IM_URL = "wss://live-ws.immomo.com/ws/im";
|
|
||||||
|
|
||||||
export const Danmu: React.FC = () => {
|
|
||||||
//Public API that will echo messages sent to it back to the client
|
|
||||||
const [socketUrl, setSocketUrl] = useState(MOMO_IM_URL);
|
|
||||||
const [messageHistory, setMessageHistory] = useState<MessageEvent<any>[]>([]);
|
|
||||||
|
|
||||||
// { "msg_id": 1, "client_time": 1712305590234, "type": "Sauth", "data": { "momoid": "404821828", "roomid": "14515460054", "role": 6, "isVisitor": false, "token": "97e721d0bd3bf2e63a9797f9daf50919", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" } }
|
|
||||||
const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl, {
|
|
||||||
heartbeat: {
|
|
||||||
message: () => {
|
|
||||||
const pingMessage = {
|
|
||||||
msg_id: 2,
|
|
||||||
client_time: Date.now(),
|
|
||||||
type: "Ping",
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
return JSON.stringify(pingMessage);
|
|
||||||
},
|
|
||||||
interval: 5000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const momoId = "404821828";
|
|
||||||
const roomId = "14515460054";
|
|
||||||
const momoToken = "97e721d0bd3bf2e63a9797f9daf50919";
|
|
||||||
|
|
||||||
const clientTime = Date.now();
|
|
||||||
const data = {
|
|
||||||
msg_id: 1,
|
|
||||||
client_time: clientTime,
|
|
||||||
type: "Sauth",
|
|
||||||
data: {
|
|
||||||
momoid: momoId,
|
|
||||||
roomid: roomId,
|
|
||||||
role: 6,
|
|
||||||
isVisitor: false,
|
|
||||||
token: momoToken,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ReadyState.OPEN === readyState) {
|
|
||||||
sendMessage(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
}, [readyState]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (lastMessage !== null) {
|
|
||||||
if (lastMessage.data) {
|
|
||||||
const message = JSON.parse(lastMessage.data);
|
|
||||||
if (message.type === "pong") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setMessageHistory((prev) => prev.concat(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [lastMessage]);
|
|
||||||
|
|
||||||
const connectionStatus = {
|
|
||||||
[ReadyState.CONNECTING]: "Connecting",
|
|
||||||
[ReadyState.OPEN]: "Open",
|
|
||||||
[ReadyState.CLOSING]: "Closing",
|
|
||||||
[ReadyState.CLOSED]: "Closed",
|
|
||||||
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
|
|
||||||
}[readyState];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{messageHistory.map((message, idx) => (
|
|
||||||
<MessageDispatcher key={idx} message={message} />
|
|
||||||
))}
|
|
||||||
<div className="chat chat-end">
|
|
||||||
<div className="chat-bubble">You underestimate my power!</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MessageDispatcher: React.FC<{ message: any }> = ({ message }) => {
|
|
||||||
const messageType = message?.groups?.[0]?.units?.[0]?.type;
|
|
||||||
let nick = "wait";
|
|
||||||
let content = "wait";
|
|
||||||
|
|
||||||
if (messageType === "Message") {
|
|
||||||
nick = message?.groups?.[0]?.nick;
|
|
||||||
content = message?.groups?.[0]?.units?.[0]?.["Msg.Message.data"]?.text;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="chat chat-start">
|
|
||||||
<div className="chat-bubble">
|
|
||||||
{nick}: {content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
import { Danmu } from "./danmu";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return <Danmu />;
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import musicSvg from "@/app/music.svg";
|
import musicSvg from "@/app/music.svg";
|
||||||
|
|
||||||
export default function Home() {
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
<section className="text-gray-600 body-font">
|
<section className="text-gray-600 body-font">
|
||||||
<div className="container mx-auto flex px-5 py-24 md:flex-row flex-col items-center">
|
<div className="container mx-auto flex px-5 py-24 md:flex-row flex-col items-center">
|
||||||
@ -18,12 +18,12 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Link
|
<Link
|
||||||
href="/music"
|
href="/danmu"
|
||||||
className="inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded text-lg"
|
className="inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded text-lg"
|
||||||
>
|
>
|
||||||
弹幕测试
|
弹幕测试
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/danmu" className="ml-4 inline-flex text-gray-700 bg-gray-100 border-0 py-2 px-6 focus:outline-none hover:bg-gray-200 rounded text-lg">
|
<Link href="/songs" className="ml-4 inline-flex text-gray-700 bg-gray-100 border-0 py-2 px-6 focus:outline-none hover:bg-gray-200 rounded text-lg">
|
||||||
歌词曲库
|
歌词曲库
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,7 +23,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
action={dispatch}
|
action={dispatch}
|
||||||
className="lg:w-2/6 md:w-1/2 bg-gray-100 rounded-lg p-8 flex flex-col md:ml-auto w-full mt-10 md:mt-0"
|
className="lg:w-2/6 md:w-1/2 bg-gray-100 rounded-lg p-8 flex flex-col md:ml-auto w-full mt-10 md:mt-0 shadow-xl"
|
||||||
>
|
>
|
||||||
<h2 className="text-gray-900 text-lg font-medium title-font mb-5">
|
<h2 className="text-gray-900 text-lg font-medium title-font mb-5">
|
||||||
登录
|
登录
|
||||||
|
@ -22,7 +22,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
action={dispatch}
|
action={dispatch}
|
||||||
className="lg:w-2/6 md:w-1/2 bg-gray-100 rounded-lg p-8 flex flex-col md:ml-auto w-full mt-10 md:mt-0"
|
className="lg:w-2/6 md:w-1/2 bg-gray-100 rounded-lg p-8 flex flex-col md:ml-auto w-full mt-10 md:mt-0 shadow-xl"
|
||||||
>
|
>
|
||||||
<h2 className="text-gray-900 text-lg font-medium title-font mb-5">
|
<h2 className="text-gray-900 text-lg font-medium title-font mb-5">
|
||||||
注册
|
注册
|
||||||
|
53
components/token-button.tsx
Normal file
53
components/token-button.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
import { addToken } from "@/lib/actions/token";
|
||||||
|
import { Token } from "@/lib/types";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useFormState } from "react-dom";
|
||||||
|
|
||||||
|
export default function TokenButton({
|
||||||
|
className,
|
||||||
|
token,
|
||||||
|
}: {
|
||||||
|
className?: string | undefined;
|
||||||
|
token: Token;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDialogElement>(null);
|
||||||
|
const [returnType, dispatch] = useFormState(addToken, token);
|
||||||
|
// TODO 有没有更合适的方式?
|
||||||
|
if (returnType) {
|
||||||
|
ref.current?.close();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button className={className} onClick={() => ref.current?.showModal()}>
|
||||||
|
Token 设定
|
||||||
|
</button>
|
||||||
|
<dialog className="modal" ref={ref}>
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">Token 设定</h3>
|
||||||
|
<form className="join join-vertical w-full mt-4" id="token" action={dispatch}>
|
||||||
|
<label className="input input-bordered flex items-center gap-2 join-item">
|
||||||
|
陌陌 ID
|
||||||
|
<input type="text" className="grow" name="uid" defaultValue={token.uid} />
|
||||||
|
</label>
|
||||||
|
<label className="input input-bordered flex items-center gap-2 join-item">
|
||||||
|
陌陌 token
|
||||||
|
<input type="text" className="grow" name="token" defaultValue={token.token} />
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
<div className="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<button className="btn">关闭</button>
|
||||||
|
</form>
|
||||||
|
<button className="btn" form="token">
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button>关闭</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
57
lib/actions/token.ts
Normal file
57
lib/actions/token.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidateTag } from "next/cache";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { Token } from "../types";
|
||||||
|
|
||||||
|
export async function addToken(_: Token, item: FormData): Promise<Token> {
|
||||||
|
const token = cookies().get("token");
|
||||||
|
const result = await fetch(
|
||||||
|
"https://tachy.daoyoucloud.com/api/tokens:create",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-App": "danmu-sim",
|
||||||
|
"X-Authenticator": "basic",
|
||||||
|
authorization: "Bearer " + token?.value,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
uid: item.get("uid") as string,
|
||||||
|
token: item.get("token") as string,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const latest = await result.json();
|
||||||
|
revalidateTag("tokens");
|
||||||
|
return latest.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getToken(): Promise<Token> {
|
||||||
|
const token = cookies().get("token");
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("pageSize", "20");
|
||||||
|
params.append("limit", "1");
|
||||||
|
params.append("sort", "-createdAt");
|
||||||
|
params.append(
|
||||||
|
"filter",
|
||||||
|
JSON.stringify({ $and: [{ createdBy: { id: { $eq: "{{$user.id}}" } } }] })
|
||||||
|
);
|
||||||
|
const liveTokens = await fetch(
|
||||||
|
"https://tachy.daoyoucloud.com/api/tokens:list?" + params.toString(),
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-App": "danmu-sim",
|
||||||
|
"X-Authenticator": "basic",
|
||||||
|
authorization: "Bearer " + token?.value,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
tags: ["tokens"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const result = await liveTokens.json();
|
||||||
|
return result?.data?.[0] ?? { uid: "", token: "" };
|
||||||
|
}
|
@ -5,3 +5,8 @@ export enum LyricSource {
|
|||||||
NETEASE = "NetEase",
|
NETEASE = "NetEase",
|
||||||
QQMUSIC = "QQMusic",
|
QQMUSIC = "QQMusic",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
uid: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user