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 */
|
||||
"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 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);
|
||||
import { getToken } from "@/lib/actions/token";
|
||||
import Controls from "./controls";
|
||||
import { Danmu } from "./danmu";
|
||||
|
||||
export default async function Page() {
|
||||
const token = await getToken();
|
||||
return (
|
||||
<div className="mockup-browser border">
|
||||
<div className="mockup-browser-toolbar">
|
||||
<div className="input">红豆</div>
|
||||
</div>
|
||||
<div className="flex justify-center px-4 py-16 border-t border-base-300">
|
||||
<div className="columns-1">
|
||||
<div className="w-full">
|
||||
<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 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-toolbar">
|
||||
<div className="input">红豆</div>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-300">
|
||||
<Controls />
|
||||
<Danmu token={token} />
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import Logo from "@/components/logo";
|
||||
import BackButton from "@/components/back-button";
|
||||
import Link from "next/link";
|
||||
import { AdminButton } from "@/components/admin-button";
|
||||
import TokenButton from "@/components/token-button";
|
||||
import { getToken } from "@/lib/actions/token";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@ -14,11 +16,12 @@ export const metadata: Metadata = {
|
||||
description: "搜索歌曲,选择直播间,智能发送弹幕",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const token = await getToken();
|
||||
return (
|
||||
<html lang="zh_CN" data-theme="light">
|
||||
<body className={inter.className}>
|
||||
@ -38,7 +41,7 @@ export default function RootLayout({
|
||||
<Link href="/songs" className="mr-5 hover:text-gray-900">
|
||||
歌词曲库
|
||||
</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 href="/danmu" className="mr-5 hover:text-gray-900">
|
||||
@ -49,8 +52,8 @@ export default function RootLayout({
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="join">
|
||||
<BackButton className="btn join-item" />
|
||||
<AdminButton className="btn join-item" />
|
||||
<TokenButton className="btn join-item" token={token} />
|
||||
<SignoutButton className="btn join-item" />
|
||||
</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 musicSvg from "@/app/music.svg";
|
||||
|
||||
export default function Home() {
|
||||
export default async function Home() {
|
||||
return (
|
||||
<section className="text-gray-600 body-font">
|
||||
<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>
|
||||
<div className="flex justify-center">
|
||||
<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"
|
||||
>
|
||||
弹幕测试
|
||||
</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>
|
||||
</div>
|
||||
|
@ -23,7 +23,7 @@ export default function Page() {
|
||||
</div>
|
||||
<form
|
||||
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">
|
||||
登录
|
||||
|
@ -22,7 +22,7 @@ export default function Page() {
|
||||
</div>
|
||||
<form
|
||||
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">
|
||||
注册
|
||||
|
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",
|
||||
QQMUSIC = "QQMusic",
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
uid: string;
|
||||
token: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user