diff options
| -rw-r--r-- | pnpm-lock.yaml | 28 | ||||
| -rw-r--r-- | src/app/create/page.tsx | 243 | ||||
| -rw-r--r-- | src/app/game/page.tsx | 20 | ||||
| -rw-r--r-- | src/app/page.tsx | 6 |
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> |
