aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2023-11-15 00:28:35 -0800
committerPinapelz <yukais@pinapelz.com>2023-11-15 00:28:35 -0800
commit71b680fb9d29057b97748c54d1ad20229fe3394c (patch)
tree1fb4dc09865857cd8705ac5b35a59fbfa951d22b
parentc789256ebd5691b805c81bd673a153a984b3039e (diff)
feat: add custom upload for lrc and video/audio
-rw-r--r--package.json7
-rw-r--r--pnpm-lock.yaml181
-rw-r--r--src/app/App.tsx151
-rw-r--r--src/app/components/KaraokePlayer.tsx58
-rw-r--r--src/app/data.ts66
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]`;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage