diff options
| author | Pinapelz <yukais@pinapelz.com> | 2023-11-15 00:28:35 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2023-11-15 00:28:35 -0800 |
| commit | 71b680fb9d29057b97748c54d1ad20229fe3394c (patch) | |
| tree | 1fb4dc09865857cd8705ac5b35a59fbfa951d22b | |
| parent | c789256ebd5691b805c81bd673a153a984b3039e (diff) | |
feat: add custom upload for lrc and video/audio
| -rw-r--r-- | package.json | 7 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 181 | ||||
| -rw-r--r-- | src/app/App.tsx | 151 | ||||
| -rw-r--r-- | src/app/components/KaraokePlayer.tsx | 58 | ||||
| -rw-r--r-- | src/app/data.ts | 66 |
5 files changed, 338 insertions, 125 deletions
diff --git a/package.json b/package.json index 5b0f1eb..d6858a3 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "dependencies": { "next": "14.0.2", "react": "^18", + "react-bootstrap": "^2.9.1", "react-dom": "^18", "react-lrc": "^3.0.2", + "react-toastify": "^9.1.3", "styled-components": "^6.1.1" }, "devDependencies": { @@ -25,5 +27,10 @@ "postcss": "^8", "tailwindcss": "^3.3.0", "typescript": "^5" + }, + "browser": { + "fs": false, + "path": false, + "os": false } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 813a706..90dcfe9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,18 @@ dependencies: react: specifier: ^18 version: 18.2.0 + react-bootstrap: + specifier: ^2.9.1 + version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) react-lrc: specifier: ^3.0.2 version: 3.0.2(react-dom@18.2.0)(react@18.2.0) + react-toastify: + specifier: ^9.1.3 + version: 9.1.3(react-dom@18.2.0)(react@18.2.0) styled-components: specifier: ^6.1.1 version: 6.1.1(react-dom@18.2.0)(react@18.2.0) @@ -67,7 +73,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 - dev: true /@emotion/is-prop-valid@1.2.1: resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} @@ -282,6 +287,48 @@ packages: fastq: 1.15.0 dev: true + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + + /@react-aria/ssr@3.9.0(react@18.2.0): + resolution: {integrity: sha512-Bz6BqP6ZorCme9tSWHZVmmY+s7AU8l6Vl2NUYmBzezD//fVHHfFo4lFBn5tBuAaJEm3AuCLaJQ6H2qhxNSb7zg==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@swc/helpers': 0.5.2 + react: 18.2.0 + dev: false + + /@restart/hooks@0.4.11(react@18.2.0): + resolution: {integrity: sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + dequal: 2.0.3 + react: 18.2.0 + dev: false + + /@restart/ui@1.6.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-eC3puKuWE1SRYbojWHXnvCNHGgf3uzHCb6JOhnF4OXPibOIPEkR1sqDSkL643ydigxwh+ruCa1CmYHlzk7ikKA==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + dependencies: + '@babel/runtime': 7.23.2 + '@popperjs/core': 2.11.8 + '@react-aria/ssr': 3.9.0(react@18.2.0) + '@restart/hooks': 0.4.11(react@18.2.0) + '@types/warning': 3.0.3 + dequal: 2.0.3 + dom-helpers: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + uncontrollable: 8.0.4(react@18.2.0) + warning: 4.0.3 + dev: false + /@rushstack/eslint-patch@1.5.1: resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==} dev: true @@ -304,7 +351,6 @@ packages: /@types/prop-types@15.7.10: resolution: {integrity: sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==} - dev: true /@types/react-dom@18.2.15: resolution: {integrity: sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==} @@ -312,22 +358,30 @@ packages: '@types/react': 18.2.37 dev: true + /@types/react-transition-group@4.4.9: + resolution: {integrity: sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg==} + dependencies: + '@types/react': 18.2.37 + dev: false + /@types/react@18.2.37: resolution: {integrity: sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==} dependencies: '@types/prop-types': 15.7.10 '@types/scheduler': 0.16.6 csstype: 3.1.2 - dev: true /@types/scheduler@0.16.6: resolution: {integrity: sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==} - dev: true /@types/stylis@4.2.3: resolution: {integrity: sha512-86XLCVEmWagiUEbr2AjSbeY4qHN9jMm3pgM3PuBYfLIbT0MpDSnA3GA/4W7KoH/C/eeK77kNaeIxZzjhKYIBgw==} dev: false + /@types/warning@3.0.3: + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + dev: false + /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -664,6 +718,10 @@ packages: fsevents: 2.3.3 dev: true + /classnames@2.3.2: + resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + dev: false + /client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false @@ -672,6 +730,11 @@ packages: resolution: {integrity: sha512-tcHQInuZ8YzZCSeBpBZJtzQBSpRPs4foO8sp7dmFwTsgCF+hCWvm+iwiveUc44lxw0Em6bzbH8Fev556EqCKhQ==} dev: false + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -775,7 +838,6 @@ packages: /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - dev: true /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -806,6 +868,13 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.23.2 + csstype: 3.1.2 + dev: false + /electron-to-chromium@1.4.583: resolution: {integrity: sha512-93y1gcONABZ7uqYe/JWDVQP/Pj/sQSunF0HVAPdlg/pfBnOyBMLlQUxWvkqcljJg1+W6cjvPuYD+r1Th9Tn8mA==} dev: true @@ -1489,6 +1558,12 @@ packages: side-channel: 1.0.4 dev: true + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -1874,7 +1949,6 @@ packages: /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: true /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} @@ -2097,13 +2171,22 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /prop-types-extra@1.1.1(react@18.2.0): + resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} + peerDependencies: + react: '>=0.14.0' + dependencies: + react: 18.2.0 + react-is: 16.13.1 + warning: 4.0.3 + dev: false + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: true /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} @@ -2114,6 +2197,33 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /react-bootstrap@2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ezgmh/ARCYp18LbZEqPp0ppvy+ytCmycDORqc8vXSKYV3cer4VH7OReV8uMOoKXmYzivJTxgzGHalGrHamryHA==} + peerDependencies: + '@types/react': '>=16.14.8' + react: '>=16.14.0' + react-dom: '>=16.14.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@restart/hooks': 0.4.11(react@18.2.0) + '@restart/ui': 1.6.6(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.37 + '@types/react-transition-group': 4.4.9 + classnames: 2.3.2 + dom-helpers: 5.2.1 + invariant: 2.2.4 + prop-types: 15.8.1 + prop-types-extra: 1.1.1(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + uncontrollable: 7.2.1(react@18.2.0) + warning: 4.0.3 + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -2126,7 +2236,10 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true + + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false /react-lrc@3.0.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-PfI7Sf6IMm2h5laowtHRPG2abkJGNSf7Ha9hSFCXU2uTWIdLa8ZgR5QarbkkWgFceU+BFg/8L6zQTEBYzD3EVg==} @@ -2140,6 +2253,31 @@ packages: resize-observer-polyfill: 1.5.1 dev: false + /react-toastify@9.1.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + clsx: 1.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.23.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -2174,7 +2312,6 @@ packages: /regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - dev: true /regexp.prototype.flags@1.5.1: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} @@ -2602,6 +2739,26 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /uncontrollable@7.2.1(react@18.2.0): + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + dependencies: + '@babel/runtime': 7.23.2 + '@types/react': 18.2.37 + invariant: 2.2.4 + react: 18.2.0 + react-lifecycles-compat: 3.0.4 + dev: false + + /uncontrollable@8.0.4(react@18.2.0): + resolution: {integrity: sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==} + peerDependencies: + react: '>=16.14.0' + dependencies: + react: 18.2.0 + dev: false + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true @@ -2627,6 +2784,12 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + dev: false + /watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} diff --git a/src/app/App.tsx b/src/app/App.tsx index ebe2d33..871e387 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,7 +1,8 @@ -import { CSSProperties, useCallback, useRef, useEffect, useState } from "react"; -import styled, { css } from "styled-components"; -import { Lrc, LrcLine } from "react-lrc"; -import { LRC } from "./data"; +import React, { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; +import KaraokePlayer from './components/KaraokePlayer'; +import { toast, ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; const Root = styled.div` position: absolute; @@ -9,79 +10,129 @@ const Root = styled.div` height: 100%; top: 0; left: 0; - display: flex; flex-direction: column; + align-items: center; + background-color: #f5f5f5; `; -const lrcStyle: CSSProperties = { - flex: 1, - minHeight: 0, - overflow: 'hidden !important' -}; -const Line = styled.div<{ $active: boolean; $next: boolean }>` - min-height: 10px; - padding: 14px 30px; - font-size: 40px; - font-family : "Roboto", sans-serif; - font-weight: 500; - text-align: center; - color: rgb(72,72,72); +const FileInputContainer = styled.div` + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; + padding: 10px; + border-radius: 5px; + background-color: #ffffff; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +`; - background: linear-gradient(to right, rgba(0,0,0,0) 50%, rgb(200, 190, 190) 50%); - background-size: 200% 100%; - background-position: right bottom; +const FileInput = styled.input` + padding: 10px 15px; + border-radius: 5px; + border: 1px solid #ddd; + cursor: pointer; + display: none; + &:hover, &:focus { + background-color: #eaeaea; + outline: none; + } +`; - ${({ $active }) => $active && css` - color: black; - font-weight: 700; - background-position: left bottom; - color: rgb(50, 50, 50); - `} +const FileInputLabel = styled.label` + padding: 10px 15px; + border-radius: 5px; + border: 1px solid #ddd; + cursor: pointer; + &:hover, &:focus { + background-color: #eaeaea; + outline: none; + } `; + function App() { + const [currentMillisecond, setCurrentMillisecond] = useState(0); - + const [lrcContent, setLrcContent] = useState(''); + const [videoUrl, setVideoUrl] = useState(''); + const [showFileInputs, setShowFileInputs] = useState(true); const videoRef = useRef<HTMLVideoElement>(null); + const [offset, setOffset] = useState('0'); + const handleLrcFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + setLrcContent(e.target?.result as string); + if (videoUrl) setShowFileInputs(false); + }; + reader.readAsText(file); + toast.success("LRC file loaded successfully", { autoClose: 2000 }); + } + }; + + const handleVideoFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setVideoUrl(url); + setShowFileInputs(true); + toast.success("Video file loaded successfully", { autoClose: 2000 }); + } + }; useEffect(() => { const video = videoRef.current; if (!video) return; const syncLrcWithVideo = () => { - const offset = 400; - setCurrentMillisecond((video.currentTime * 1000)+offset); + console.log(offset); + setCurrentMillisecond((video.currentTime * 1000) + parseInt(offset)); }; - video.addEventListener('timeupdate', syncLrcWithVideo); return () => { video.removeEventListener('timeupdate', syncLrcWithVideo); }; - }, [setCurrentMillisecond]); - - const lineRenderer = useCallback( - ({ active, line: { content } }: { active: boolean; line: LrcLine }) => { - const next = active && content === ''; - return <Line $active={active} $next={next}>{content}</Line> - }, - [] - ); + }); return ( <Root> + <ToastContainer /> <div style={{ display: 'flex', width: '100%', height: '100vh' }}> - <Lrc - lrc={LRC} - lineRenderer={lineRenderer} - currentMillisecond={currentMillisecond} - style={lrcStyle} - recoverAutoScrollInterval={0} - /> - <div style={{ flex: 1 }}> - <video ref={videoRef} src="https://cdn.pinapelz.com/VTuber%20Covers%20Archive/pj9yqqTYa-E.webm" controls style={{ width: '100%', height: '100%' }} /> - + <KaraokePlayer + lrc={lrcContent} + currentMillisecond={currentMillisecond} + /> + <div style={{ flex: 1, position: 'relative' }} onMouseEnter={() => setShowFileInputs(true)} onMouseLeave={() => setShowFileInputs(false)}> + {videoUrl ? <video ref={videoRef} src={videoUrl} controls style={{ width: '100%', height: '100%' }} /> :<div style={{ width: '100%', height: '100%', backgroundColor: '#ddd', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> + <p style={{fontSize: '30px', textAlign: 'center', fontFamily:'Arial', fontWeight:'bold'}}> + Please select the video and lrc (lyrics) file <br/> + Hover over me for a menu</p> + </div> + } + {showFileInputs && ( + <FileInputContainer style={{ position: 'absolute', bottom: '20px', left: 0 }}> + <FileInputLabel htmlFor="lrcUpload" style={{ cursor: 'pointer' }}>LRC</FileInputLabel> + <FileInput id="lrcUpload" type="file" accept=".lrc" onChange={handleLrcFileChange} /> + <FileInputLabel htmlFor="videoUpload" style={{ cursor: 'pointer' }}>Video</FileInputLabel> + <FileInput id="videoUpload" type="file" accept="video/*" onChange={handleVideoFileChange} /> + <FileInputLabel htmlFor="srvUpload" style={{ cursor: 'pointer' }}>SRV</FileInputLabel> + <FileInput disabled type="file" accept=".srv" /> + <div style={{ display: 'flex', flexDirection: 'column', fontFamily: 'Arial' }}> + <label>Offset (±ms) </label> + <input + type="number" + style={{ fontSize: '20px' }} + id="numberInput" + value={offset} + onChange={(e) => setOffset(e.target.value)} + step="100" + /> + </div> + </FileInputContainer> + )} </div> </div> </Root> diff --git a/src/app/components/KaraokePlayer.tsx b/src/app/components/KaraokePlayer.tsx new file mode 100644 index 0000000..5160226 --- /dev/null +++ b/src/app/components/KaraokePlayer.tsx @@ -0,0 +1,58 @@ +import React, { CSSProperties, useCallback } from 'react'; +import styled, { css } from 'styled-components'; +import { Lrc, LrcLine } from 'react-lrc'; + +const Line = styled.div<{ $active: boolean; $next: boolean }>` + min-height: 10px; + padding: 14px 30px; + + font-size: 40px; + font-family: "Roboto", sans-serif; + font-weight: 500; + text-align: center; + color: rgb(72,72,72); + + background: linear-gradient(to right, rgba(0,0,0,0) 50%, rgb(200, 190, 190) 50%); + background-size: 200% 100%; + background-position: right bottom; + transition: color 1s ease-out, background-position 1s ease-out; + + ${({ $active }) => $active && css` + color: black; + font-weight: 700; + background-position: left bottom; + color: rgb(50, 50, 50); + `} +`; +const lrcStyle: CSSProperties = { + flex: 1, + minHeight: 0, + overflow: 'hidden !important' +}; + +interface KaraokePlayerProps { + currentMillisecond: number; + lrc: string; +} + +const KaraokePlayer: React.FC<KaraokePlayerProps> = ({ currentMillisecond, lrc }) => { + const lineRenderer = useCallback( + ({ active, line: { content } }: { active: boolean; line: LrcLine }) => { + const next = active && content === ''; + return <Line $active={active} $next={next}>{content}</Line>; + }, + [] + ); + + return ( + <Lrc + lrc={lrc} + lineRenderer={lineRenderer} + currentMillisecond={currentMillisecond} + style={lrcStyle} + recoverAutoScrollInterval={0} + /> + ); +}; + +export default KaraokePlayer; diff --git a/src/app/data.ts b/src/app/data.ts deleted file mode 100644 index eff48cf..0000000 --- a/src/app/data.ts +++ /dev/null @@ -1,66 +0,0 @@ -export const LRC = `[00:11.53]Another day, I wander -[00:13.85]Without escape, I ponder -[00:15.98]A million questions -[00:17.25]I don't need to find an answer for -[00:20.59]Like, is it worth the hassle? -[00:22.78]Or, is it worth the pain? -[00:24.23]Look inside the mirror and say to myself -[00:28.17]Am I enough? -[00:30.81]Oh, am I worth it? -[00:35.570] -[00:37.670]I see the way you notice -[00:39.980]All of my fragile moments -[00:42.210]A part of me is still uncertain I should let you in -[00:46.670]But as it flows and passes -[00:48.800]The time will just confirm -[00:50.150]That when you're here with me -[00:51.940]I can just let go -[00:54.560]Now suddenly, the clouds clear out -[00:59.070]All my worries disappear -[01:01.070]And all the stars, they feel so near -[01:03.340]I could almost reach out right now -[01:07.580]Because of you, feel myself again -[01:11.980]You helped me realize what was here to do -[01:16.480]Because of you, I'm feeling real again -[01:20.840]Now I know where I should go -[01:22.940]You got me walking back to hope -[01:25.090]One step at a time -[01:34.560]I can't explain the feeling -[01:36.630]A sort of, kind of healing -[01:38.980]A different wave of love that travels -[01:41.530]Through your precious words -[01:43.370]At times, I circle back, but -[01:45.600]I guess I just forget -[01:46.980]That when I'm down and low -[01:48.940]I could count on you -[01:51.460]Now suddenly, the clouds clear out -[01:55.760]All my worries disappear -[01:57.810]And all the stars, they feel so near -[02:00.300]I could almost reach out right now -[02:05.530]Because of you, I feel myself again -[02:09.930]You helped me realize what I was here to do -[02:14.370]Because of you, I'm feeling real again -[02:18.520]Now I know where I should go -[02:20.640]You got me walking back to hope -[02:22.250]A step at a time -[02:25.940]♪ -[02:41.030]What can I say but thank you -[02:43.400]Surrounded by some angels -[02:45.190]Honestly, it's hard for me to live without -[02:49.800]Tomorrow's bringing something new -[02:53.160]I know for certain, it's worth it -[02:56.560]All thanks to you -[03:08.840]Because of you, I feel myself again -[03:13.240]You helped me realize what I was here to do -[03:17.450]Because of you, I'm feeling real again -[03:21.930]Now I know where I should go -[03:23.980]You got me walking back to hope -[03:26.390]Because of you, I feel myself again -[03:30.700]You helped me realize what I was here to do -[03:34.940]Because of you, I'm feeling real again -[03:39.310]Now I know where I should go -[03:41.570]You got me walking back to hope -[03:43.860]One step at a time -[03:51.980]A little closer -[03:55.580]Every step gets closer -[03:58.330]`; |
