aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pnpm-lock.yaml28
-rw-r--r--src/app/create/page.tsx243
-rw-r--r--src/app/game/page.tsx20
-rw-r--r--src/app/page.tsx6
4 files changed, 197 insertions, 100 deletions
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 58e692c..548d74e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -228,105 +228,89 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
- libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -393,28 +377,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@next/swc-linux-arm64-musl@16.2.3':
resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@next/swc-linux-x64-gnu@16.2.3':
resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@next/swc-linux-x64-musl@16.2.3':
resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@next/swc-win32-arm64-msvc@16.2.3':
resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==}
@@ -613,49 +593,41 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx
index 5bcca2a..016c930 100644
--- a/src/app/create/page.tsx
+++ b/src/app/create/page.tsx
@@ -1,5 +1,5 @@
"use client";
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { MdLibraryMusic } from "react-icons/md";
import { FaCopy, FaCheck, FaExternalLinkAlt } from "react-icons/fa";
import { Root, Navbar, Logo, LogoIcon, NavLink } from "../styles/shared";
@@ -21,7 +21,9 @@ import {
OpenLink,
} from "./page.styles";
-interface Payload {
+type CreateMode = "karaoke" | "typing";
+
+interface KaraokePayload {
lrc?: string;
srv3?: string;
file1?: string;
@@ -30,7 +32,20 @@ interface Payload {
offset2?: number;
}
+interface TypingPayload {
+ file1?: string;
+ lrc?: string;
+ offset?: number;
+ title?: string;
+ artist?: string;
+ skip_backing?: boolean;
+ difficulty?: number;
+}
+
export default function CreatePage() {
+ const [mode, setMode] = useState<CreateMode>("karaoke");
+
+ // Karaoke fields
const [lrc, setLrc] = useState("");
const [srv3, setSrv3] = useState("");
const [file1, setFile1] = useState("");
@@ -38,21 +53,47 @@ export default function CreatePage() {
const [offset, setOffset] = useState("");
const [offset2, setOffset2] = useState("");
+ // Typing fields
+ const [typingTitle, setTypingTitle] = useState("");
+ const [typingArtist, setTypingArtist] = useState("");
+ const [typingDifficulty, setTypingDifficulty] = useState("");
+ const [skipBacking, setSkipBacking] = useState(true);
+
const [code, setCode] = useState<string | null>(null);
const [copiedCode, setCopiedCode] = useState(false);
const [copiedUrl, setCopiedUrl] = useState(false);
+ const resetCopyStates = () => {
+ setCopiedCode(false);
+ setCopiedUrl(false);
+ };
+
const generate = () => {
- const payload: Payload = {};
- if (lrc.trim()) payload.lrc = lrc.trim();
- if (srv3.trim()) payload.srv3 = srv3.trim();
+ if (mode === "karaoke") {
+ const payload: KaraokePayload = {};
+ if (lrc.trim()) payload.lrc = lrc.trim();
+ if (srv3.trim()) payload.srv3 = srv3.trim();
+ if (file1.trim()) payload.file1 = file1.trim();
+ if (file2.trim()) payload.file2 = file2.trim();
+ if (offset.trim() !== "") payload.offset = Number(offset);
+ if (offset2.trim() !== "") payload.offset2 = Number(offset2);
+
+ setCode(btoa(JSON.stringify(payload)));
+ resetCopyStates();
+ return;
+ }
+
+ const payload: TypingPayload = {};
if (file1.trim()) payload.file1 = file1.trim();
- if (file2.trim()) payload.file2 = file2.trim();
+ if (lrc.trim()) payload.lrc = lrc.trim();
if (offset.trim() !== "") payload.offset = Number(offset);
- if (offset2.trim() !== "") payload.offset2 = Number(offset2);
+ if (typingTitle.trim()) payload.title = typingTitle.trim();
+ if (typingArtist.trim()) payload.artist = typingArtist.trim();
+ payload.skip_backing = skipBacking;
+ if (typingDifficulty.trim() !== "") payload.difficulty = Number(typingDifficulty);
+
setCode(btoa(JSON.stringify(payload)));
- setCopiedCode(false);
- setCopiedUrl(false);
+ resetCopyStates();
};
const copy = (text: string, which: "code" | "url") => {
@@ -66,9 +107,14 @@ export default function CreatePage() {
}
};
- const shareUrl = code
- ? `${typeof window !== "undefined" ? window.location.origin : ""}/player?code=${code}`
- : "";
+ const playerPath = mode === "typing" ? "/typing" : "/player";
+ const shareUrl = useMemo(
+ () =>
+ code
+ ? `${typeof window !== "undefined" ? window.location.origin : ""}${playerPath}?code=${code}`
+ : "",
+ [code, playerPath]
+ );
return (
<Root>
@@ -83,15 +129,45 @@ export default function CreatePage() {
</Navbar>
<Content>
- <Heading>Create a Karaoke Code</Heading>
+ <Heading>Create a Code</Heading>
<Subheading>
- Fill in the URLs and offsets for your session, then generate a
- shareable code.
+ Switch between Karaoke and Typing Game modes, then generate a shareable code for your session.
</Subheading>
<Form>
+ <Row>
+ <GenerateButton
+ onClick={() => {
+ setMode("karaoke");
+ setCode(null);
+ resetCopyStates();
+ }}
+ style={{
+ backgroundColor: mode === "karaoke" ? "#1a1a1a" : "#e5e5e5",
+ color: mode === "karaoke" ? "#fff" : "#1a1a1a",
+ }}
+ >
+ MoekyunKaraoke
+ </GenerateButton>
+ <GenerateButton
+ onClick={() => {
+ setMode("typing");
+ setCode(null);
+ resetCopyStates();
+ }}
+ style={{
+ backgroundColor: mode === "typing" ? "#1a1a1a" : "#e5e5e5",
+ color: mode === "typing" ? "#fff" : "#1a1a1a",
+ }}
+ >
+ LRC-Type
+ </GenerateButton>
+ </Row>
+
+ <Divider />
+
<FieldGroup>
- <Label>Media (file1)</Label>
+ <Label>Media - The main audio that plays</Label>
<Input
type="url"
placeholder="https://example.com/song.mp4"
@@ -111,49 +187,108 @@ export default function CreatePage() {
</FieldGroup>
<FieldGroup>
- <Label>SRV3 Subtitles</Label>
+ <Label title="Offset in milliseconds. Increase this value if the main audio is ahead of the lyrics.">
+ LRC Offset (ms)
+ </Label>
<Input
- type="url"
- placeholder="https://example.com/song.srv3"
- value={srv3}
- onChange={(e) => setSrv3(e.target.value)}
+ type="number"
+ placeholder="0"
+ value={offset}
+ onChange={(e) => setOffset(e.target.value)}
+ step="25"
/>
</FieldGroup>
- <Divider />
+ {mode === "karaoke" ? (
+ <>
+ <FieldGroup>
+ <Label title="SRV3 is a YouTube-style timed text format used for subtitles. Provide a .srv3 URL to display timed subtitles in the player (optional).">
+ SRV3 Subtitles (Optional)
+ </Label>
+ <Input
+ type="url"
+ placeholder="https://example.com/song.srv3"
+ value={srv3}
+ onChange={(e) => setSrv3(e.target.value)}
+ />
+ </FieldGroup>
- <FieldGroup>
- <Label>Audio #2</Label>
- <Input
- type="url"
- placeholder="https://example.com/instrumental.mp3"
- value={file2}
- onChange={(e) => setFile2(e.target.value)}
- />
- </FieldGroup>
+ <Divider />
- <Row>
- <FieldGroup>
- <Label>LRC Offset (ms)</Label>
- <Input
- type="number"
- placeholder="0"
- value={offset}
- onChange={(e) => setOffset(e.target.value)}
- step="25"
- />
- </FieldGroup>
- <FieldGroup>
- <Label>Audio #2 Offset (ms)</Label>
- <Input
- type="number"
- placeholder="0"
- value={offset2}
- onChange={(e) => setOffset2(e.target.value)}
- step="25"
- />
- </FieldGroup>
- </Row>
+ <FieldGroup>
+ <Label>Backing Audio #2 (Optional)</Label>
+ <Input
+ type="url"
+ placeholder="https://example.com/instrumental.mp3"
+ value={file2}
+ onChange={(e) => setFile2(e.target.value)}
+ />
+ </FieldGroup>
+
+ <FieldGroup>
+ <Label title="Offset in milliseconds. Increase this value if the main audio is ahead of the backing audio.">
+ Backing Audio #2 Offset (ms)
+ </Label>
+ <Input
+ type="number"
+ placeholder="0"
+ value={offset2}
+ onChange={(e) => setOffset2(e.target.value)}
+ step="25"
+ />
+ </FieldGroup>
+ </>
+ ) : (
+ <>
+ <Divider />
+ <Row>
+ <FieldGroup>
+ <Label>Title</Label>
+ <Input
+ type="text"
+ placeholder="Song Title"
+ value={typingTitle}
+ onChange={(e) => setTypingTitle(e.target.value)}
+ />
+ </FieldGroup>
+ <FieldGroup>
+ <Label>Artist</Label>
+ <Input
+ type="text"
+ placeholder="Artist Name"
+ value={typingArtist}
+ onChange={(e) => setTypingArtist(e.target.value)}
+ />
+ </FieldGroup>
+ </Row>
+
+ <Row>
+ <FieldGroup>
+ <Label title="When enabled, lyrics inside parentheses are treated as backing lyrics and skipped.">
+ Skip Backing
+ </Label>
+ <Input
+ type="checkbox"
+ checked={skipBacking}
+ onChange={(e) => setSkipBacking(e.target.checked)}
+ style={{ width: "18px", height: "18px", marginTop: "10px" }}
+ />
+ </FieldGroup>
+
+ <FieldGroup>
+ <Label>Difficulty (number)</Label>
+ <Input
+ type="number"
+ placeholder="1"
+ min="1"
+ step="1"
+ value={typingDifficulty}
+ onChange={(e) => setTypingDifficulty(e.target.value)}
+ />
+ </FieldGroup>
+ </Row>
+ </>
+ )}
<GenerateButton onClick={generate}>Generate Code</GenerateButton>
</Form>
@@ -189,7 +324,7 @@ export default function CreatePage() {
</div>
<OpenLink href={shareUrl} target="_blank" rel="noopener noreferrer">
- <FaExternalLinkAlt /> Open in Player
+ <FaExternalLinkAlt /> Open in {mode === "typing" ? "Typing Game" : "Player"}
</OpenLink>
</OutputSection>
)}
diff --git a/src/app/game/page.tsx b/src/app/game/page.tsx
index 3bbbbac..7e6e9c3 100644
--- a/src/app/game/page.tsx
+++ b/src/app/game/page.tsx
@@ -66,7 +66,7 @@ import {
HomeBtn,
} from "./page.styles";
import { gReducer, initialGState } from "./game.stat";
-import { formatTime, parseLrcLines, type GameLine } from "./game.utils";
+import { formatTime, parseLrcLines } from "./game.utils";
type GamePhase = "idle" | "countdown" | "playing" | "paused" | "finished";
@@ -407,14 +407,14 @@ function GameInner() {
LRC-Type
</Link>
<Link
- href="/player"
+ href="/"
style={{
fontSize: 13,
color: "rgba(255,255,255,0.6)",
textDecoration: "none",
}}
>
- Player
+ Karaoke
</Link>
<Link
href="/create"
@@ -463,11 +463,11 @@ function GameInner() {
textTransform: "uppercase",
}}
>
- Load another song
+ Load a chart
</div>
<CodeInputRow>
<CodeInputField
- placeholder="Paste MoekyunKaraoke or MoekyunKaraoke+ code..."
+ placeholder="Enter a MoekyunKaraoke or LRC-Type code..."
value={codeInput}
onChange={(e) => setCodeInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleLoadCode()}
@@ -475,16 +475,6 @@ function GameInner() {
<CodeLoadBtn onClick={handleLoadCode}>Load</CodeLoadBtn>
</CodeInputRow>
</CodeSection>
- <Link
- href="/player"
- style={{
- fontSize: 13,
- color: "rgba(255,255,255,0.35)",
- textDecoration: "none",
- }}
- >
- Back to Player
- </Link>
</StartCard>
</StartOverlay>
)}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 66ef915..c1a8dc7 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -83,7 +83,7 @@ export default function HomePage() {
<LogoIcon>
<MdLibraryMusic />
</LogoIcon>
- LRC-Karaoke-Player
+ LRC-Karaoke
</Logo>
</NavLeft>
@@ -101,8 +101,8 @@ export default function HomePage() {
</NavCenter>
<NavRight>
- <NavLink href="/game">Typing Game</NavLink>
- <NavLink href="/create">Create Karaoke Code</NavLink>
+ <NavLink href="/game">LRC-Type</NavLink>
+ <NavLink href="/create">Create</NavLink>
<Avatar>
<FaUserCircle />
</Avatar>
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage