Compare commits

..

3 Commits
main ... 2.1.0

Author SHA1 Message Date
0c315972c1
ref: project structure, write basic type for lyrics layout 2024-10-22 00:22:17 +08:00
d34ac176c0
ref: started refactoring the dynamic lyrics
add: initialization of lyric lines
2024-10-20 23:14:02 +08:00
alikia2x
ba31bc4b98 improve: better lyric effect & performance
add: support for TTML
2024-08-03 21:34:39 +08:00
41 changed files with 228 additions and 1275 deletions

View File

@ -1,23 +0,0 @@
# Use the official Bun image as the base image
FROM oven/bun:latest
# Set the working directory inside the container
WORKDIR /app
# Copy the package.json and bun.lockb files to the working directory
COPY package.json bun.lockb ./
# Install dependencies
RUN bun install
# Copy the rest of the application code
COPY . .
# Build the app
RUN bun run build
# Expose the port the app runs on
EXPOSE 4173
# Command to run the application
CMD ["bun", "go"]

BIN
bun.lockb

Binary file not shown.

View File

@ -1,15 +0,0 @@
version: '3.8'
services:
aquavox:
build: .
ports:
- "4173:4173"
environment:
- NODE_ENV=production
volumes:
- .:/app
command: ["bun", "go"]
volumes:
node_modules:

View File

@ -1,6 +1,6 @@
{
"name": "aquavox",
"version": "2.3.2",
"version": "1.15.0",
"private": false,
"scripts": {
"dev": "vite dev",
@ -11,7 +11,7 @@
"test": "vitest",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"go": "PORT=4173 bun ./build"
"go": "PORT=4173 node build"
},
"devDependencies": {
"@iconify/svelte": "^4.0.2",
@ -34,27 +34,15 @@
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^1.6.0"
},
"type": "module",
"dependencies": {
"@applemusic-like-lyrics/core": "^0.1.3",
"@applemusic-like-lyrics/lyric": "^0.2.2",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@pixi/app": "^7.4.2",
"@pixi/core": "^7.4.2",
"@pixi/display": "^7.4.2",
"@pixi/filter-blur": "^7.4.2",
"@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.2",
"@pixi/sprite": "^7.4.2",
"@types/bun": "^1.1.6",
"bezier-easing": "^2.1.0",
"jotai": "^2.8.0",
"jotai-svelte": "^0.0.2",
"jss": "^10.10.0",
"jss-preset-default": "^10.10.0",
"localforage": "^1.10.0",
"lrc-parser-ts": "^1.0.3",
"music-metadata-browser": "^2.5.10",

View File

@ -29,10 +29,3 @@ h2 {
.text-shadow-none {
text-shadow: none;
}
body,
html {
position: fixed;
overflow: hidden;
overscroll-behavior: none;
}

View File

@ -5,14 +5,14 @@
<!-- <link rel="icon" href="%sveltekit.assets%/favicon.png" /> -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<link
href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
</html>

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import formatDuration from '$lib/utils/formatDuration.js';
import formatDuration from '$lib/formatDuration';
import { safePath } from '$lib/server/safePath';
describe('formatDuration test', () => {

View File

@ -2,7 +2,7 @@
import { processImage } from '$lib/graphics';
import blobToImageData from '$lib/graphics/blob2imageData';
import imageDataToBlob from '$lib/graphics/imageData2blob';
import localforage from '$lib/utils/storage';
import localforage from '$lib/storage';
export let coverId: string;
let canvas: HTMLCanvasElement;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import formatDuration from "$lib/utils/formatDuration";
import { formatViews } from "$lib/utils/formatViews";
import formatDuration from "$lib/formatDuration";
import { formatViews } from "$lib/formatViews";
export let songData: MusicMetadata;
</script>

View File

@ -1,13 +1,13 @@
<script lang="ts">
import { useAtom } from 'jotai-svelte';
import { fileListState, finalFileListState } from '$lib/state/fileList.state';
import toHumanSize from '$lib/utils/humanSize';
import formatText from '$lib/utils/formatText';
import extractFileName from '$lib/utils/extractFileName';
import getAudioMeta from '$lib/utils/getAudioCoverURL';
import convertCoverData from '$lib/utils/convertCoverData';
import toHumanSize from '$lib/humanSize';
import formatText from '$lib/formatText';
import extractFileName from '$lib/extractFileName';
import getAudioMeta from '$lib/getAudioCoverURL';
import convertCoverData from '$lib/convertCoverData';
import type { IAudioMetadata } from 'music-metadata-browser';
import formatDuration from '$lib/utils/formatDuration';
import formatDuration from '$lib/formatDuration';
const items = useAtom(fileListState);
const finalItems = useAtom(finalFileListState);
let displayItems: any[] = [];

View File

@ -1,9 +1,9 @@
<script lang="ts">
import formatDuration from '$lib/utils/formatDuration';
import formatDuration from '$lib/formatDuration';
import { onMount } from 'svelte';
import userAdjustingProgress from '$lib/state/userAdjustingProgress';
import progressBarSlideValue from '$lib/state/progressBarSlideValue';
import truncate from '$lib/utils/truncate';
import truncate from '$lib/truncate';
export let name: string;
export let singer: string = '';

View File

@ -4,21 +4,15 @@
import type { LyricPos } from './type';
import type { Spring } from '$lib/graphics/spring/spring';
const viewportWidth = document.documentElement.clientWidth;
export let line: ScriptItem;
export let index: number;
export let debugMode: Boolean;
export let lyricClick: Function;
export let initPos: LyricPos;
let ref: HTMLDivElement;
let clickMask: HTMLSpanElement;
let time = 0;
let positionX: number = 0;
let positionY: number = 0;
let opacity = 1;
let stopped = false;
let lastPosX: number | undefined = undefined;
let lastPosY: number | undefined = undefined;
let lastUpdateY: number | undefined = undefined;
@ -27,18 +21,25 @@
let springX: Spring | undefined = undefined;
let isCurrentLyric = false;
$: {
if (initPos) {
positionX = initPos.x;
positionY = initPos.y;
}
}
function updateY(timestamp: number) {
if (lastUpdateY === undefined) {
lastUpdateY = new Date().getTime();
lastUpdateY = timestamp;
}
if (springY === undefined) return;
time = (new Date().getTime() - lastUpdateY) / 1000;
time = (timestamp - lastUpdateY) / 1000;
springY.update(time);
positionY = springY.getCurrentPosition();
if (!springY.arrived() && !stopped) {
if (!springY.arrived()) {
requestAnimationFrame(updateY);
}
lastUpdateY = new Date().getTime();
lastUpdateY = timestamp;
}
function updateX(timestamp: number) {
@ -46,7 +47,7 @@
lastUpdateX = timestamp;
}
if (springX === undefined) return;
time = (new Date().getTime() - lastUpdateX) / 1000;
time = (timestamp - lastUpdateX) / 1000;
springX.update(time);
positionX = springX.getCurrentPosition();
if (!springX.arrived()) {
@ -73,27 +74,19 @@
export const setCurrent = (isCurrent: boolean) => {
isCurrentLyric = isCurrent;
opacity = isCurrent ? 1 : 0.36;
};
export const setBlur = (blur: number) => {
ref.style.filter = `blur(${blur}px)`;
};
export const update = (pos: LyricPos, delay: number = 0) => {
if (lastPosX === undefined || lastPosY === undefined) {
if (lastPosX === undefined) {
lastPosX = pos.x;
}
if (lastPosY === undefined) {
lastPosY = pos.y;
}
springX!.setTargetPosition(pos.x, delay);
springY!.setTargetPosition(pos.y, delay);
lastUpdateY = new Date().getTime();
lastUpdateX = new Date().getTime();
stopped = false;
springY = createSpring(lastPosY, pos.y, 0.12, 0.7, delay);
springX = createSpring(lastPosX, pos.x, 0.12, 0.7, delay);
requestAnimationFrame(updateY);
requestAnimationFrame(updateX);
lastPosX = pos.x;
lastPosY = pos.y;
};
export const getInfo = () => {
@ -109,66 +102,16 @@
lastPosY = pos.y;
positionX = pos.x;
positionY = pos.y;
springX = createSpring(pos.x, pos.x, 0.114, 0.72);
springY = createSpring(pos.y, pos.y, 0.114, 0.72);
};
export const stop = () => {
stopped = true;
};
export const getRef = () => ref;
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
style="transform: translate3d({positionX}px, {positionY}px, 0); transition-property: opacity, text-shadow;
transition-duration: 0.36s; transition-timing-function: ease-out; opacity: {opacity};
transform-origin: center left;"
class="absolute z-50 w-full pr-12 lg:pr-16 cursor-default py-5"
bind:this={ref}
on:touchstart={() => {
clickMask.style.backgroundColor = 'rgba(255,255,255,.3)';
}}
on:touchend={() => {
clickMask.style.backgroundColor = 'transparent';
}}
on:click={() => {
lyricClick(index);
}}
>
<span
class="absolute w-[calc(100%-2.5rem)] lg:w-[calc(100%-3rem)] h-full
-translate-x-2 lg:-translate-x-5 -translate-y-5 rounded-lg duration-300 lg:hover:bg-[rgba(255,255,255,.15)]"
bind:this={clickMask}
>
</span>
<div style="transform: translateY({positionY}px) translateX({positionX}px);" class="absolute z-50" bind:this={ref}>
{#if debugMode}
<span class="text-lg absolute -translate-y-7">
{index}: duration: {(line.end - line.start).toFixed(3)}, {line.start.toFixed(3)}~{line.end.toFixed(3)}
</span>
<span class="text-lg absolute -translate-y-7">Line idx: {index}</span>
{/if}
<span
class={`text-white text-[2rem] leading-9 lg:text-5xl lg:leading-[4rem] font-semibold text-shadow-lg mr-4
${isCurrentLyric ? 'text-glow' : ''}`}
>
<span class={`text-white text-5xl font-semibold text-shadow-lg ${!isCurrentLyric && 'opacity-50'} `}>
{line.text}
</span>
{#if line.translation}
<br />
<span class={`pl-2 relative text-xl lg:text-2xl top-2 duration-300`}>
{line.translation}
</span>
{/if}
</div>
<style>
.text-glow {
text-shadow:
0 0 3px #ffffff2c,
0 0 6px #ffffff2c,
0 15px 30px rgba(0, 0, 0, 0.11),
0 5px 15px rgba(0, 0, 0, 0.08);
}
</style>

View File

@ -3,8 +3,10 @@
import createLyricsSearcher from '$lib/lyrics/lyricSearcher';
import progressBarRaw from '$lib/state/progressBarRaw';
import type { LrcJsonData } from '$lib/lyrics/type';
import progressBarSlideValue from '$lib/state/progressBarSlideValue';
import nextUpdate from '$lib/state/nextUpdate';
import truncate from '$lib/utils/truncate';
import truncate from '$lib/truncate';
import { blur } from 'svelte/transition';
// Component input properties
export let lyrics: string[];

View File

@ -3,17 +3,7 @@
import { onMount } from 'svelte';
import type { ScriptItem } from '$lib/lyrics/type';
import LyricLine from './lyricLine.svelte';
import createLyricsSearcher from '$lib/lyrics/lyricSearcher';
// constants
const viewportHeight = document.documentElement.clientHeight;
const viewportWidth = document.documentElement.clientWidth;
const marginY = viewportWidth > 640 ? 12 : 0 ;
const blurRatio = viewportWidth > 640 ? 1 : 1.4;
const currentLyrictTop = viewportWidth > 640 ? viewportHeight * 0.12 : viewportHeight * 0.05;
const deceleration = 0.95; // Velocity decay factor for inertia
const minVelocity = 0.1; // Minimum velocity to stop inertia
document.body.style.overflow = 'hidden';
import type { LyricPos } from './type';
// Props
export let originalLyrics: LrcJsonData;
@ -25,39 +15,22 @@
let lyricExists = false;
let lyricsContainer: HTMLDivElement | null;
let debugMode = false;
let nextUpdate = 0;
let lastProgress = 0;
let showTranslation = false;
let scrollEventAdded = false;
let scrolling = false;
let scrollingTimeout: Timer;
let lastY: number; // For tracking touch movements
let lastTime: number; // For tracking time between touch moves
let velocityY = 0; // Vertical scroll velocity
let inertiaFrame: number; // For storing the requestAnimationFrame reference
// Exlpaination:
// The hot module reloading makes the each lyric component position to be re-initialized,
// which causes the lyrics are all at {x: 0, y: 0},
// instead of the value calculated in the initLyricComponents.
// So, we need to store the initial position of each lyric component and restore it.
let lyricComponentInitPos = Array<LyricPos>(lyricLines.length).fill({ x: 0, y: 0 });
// References to lyric elements
let lyricElements: HTMLDivElement[] = [];
let lyricComponents: LyricLine[] = [];
let lyricTopList: number[] = [];
let currentLyricIndex: number;
$: getLyricIndex = createLyricsSearcher(originalLyrics);
$: {
currentLyricIndex = getLyricIndex(progress);
}
function initLyricComponents() {
initLyricTopList();
for (let i = 0; i < lyricComponents.length; i++) {
lyricComponents[i].init({ x: 0, y: lyricTopList[i] });
}
}
function initLyricTopList() {
let cumulativeHeight = currentLyrictTop;
let cumulativeHeight = 0;
const marginY = 48;
for (let i = 0; i < lyricLines.length; i++) {
const c = lyricComponents[i];
lyricElements.push(c.getRef());
@ -65,31 +38,7 @@
const elementHeight = e.getBoundingClientRect().height;
const elementTargetTop = cumulativeHeight;
cumulativeHeight += elementHeight + marginY;
lyricTopList.push(elementTargetTop);
}
}
function computeLayout() {
if (!originalLyrics.scripts) return;
const currentLyricDuration =
originalLyrics.scripts[currentLyricIndex].end - originalLyrics.scripts[currentLyricIndex].start;
const relativeOrigin = lyricTopList[currentLyricIndex] - currentLyrictTop;
for (let i = 0; i < lyricElements.length; i++) {
const currentLyricComponent = lyricComponents[i];
let delay = 0;
if (i < currentLyricIndex) {
delay = 0;
}
else if (i == currentLyricIndex) {
delay = 0.042;
}
else {
delay = Math.min(Math.min(currentLyricDuration, 0.6), 0.067 * (i - currentLyricIndex+1.2));
}
const offset = Math.abs(i - currentLyricIndex);
let blurRadius = Math.min(offset * blurRatio, 16);
currentLyricComponent.setBlur(blurRadius);
currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay);
lyricComponentInitPos[i] = { x: 0, y: elementTargetTop };
}
}
@ -106,115 +55,6 @@
}
}
function handleScroll(deltaY: number) {
for (let i = 0; i < lyricElements.length; i++) {
const currentLyricComponent = lyricComponents[i];
const currentY = currentLyricComponent.getInfo().y;
currentLyricComponent.setBlur(0);
currentLyricComponent.stop();
currentLyricComponent.setY(currentY - deltaY);
}
scrolling = true;
if (scrollingTimeout) clearTimeout(scrollingTimeout);
scrollingTimeout = setTimeout(() => {
scrolling = false;
}, 5000);
}
// Handle the touch start event
function handleTouchStart(event: TouchEvent) {
lastY = event.touches[0].clientY;
}
// Handle the touch move event
function handleTouchMove(event: TouchEvent) {
const currentY = event.touches[0].clientY;
const currentTime = Date.now();
const deltaY = lastY - currentY; // Calculate vertical swipe distance
const deltaTime = currentTime - lastTime;
// Calculate the scroll velocity (change in Y over time)
if (deltaTime > 0) {
velocityY = deltaY / deltaTime;
}
handleScroll(deltaY); // Simulate the scroll event
lastY = currentY; // Update lastY for the next move event
lastTime = currentTime; // Update the lastTime for the next move event
}
// Handle the touch end event
function handleTouchEnd() {
// Start inertia scrolling based on the velocity
function inertiaScroll() {
if (Math.abs(velocityY) < minVelocity) {
cancelAnimationFrame(inertiaFrame);
return;
}
handleScroll(velocityY * 16); // Multiply by frame time (16ms) to get smooth scroll
velocityY *= deceleration; // Apply deceleration to velocity
inertiaFrame = requestAnimationFrame(inertiaScroll); // Continue scrolling in next frame
}
inertiaScroll();
}
$: {
if (lyricsContainer && !scrollEventAdded) {
// Wheel event for desktop
lyricsContainer.addEventListener(
'wheel',
(e) => {
e.preventDefault();
const deltaY = e.deltaY;
handleScroll(deltaY);
},
{ passive: false }
);
// Touch events for mobile
lyricsContainer.addEventListener('touchstart', handleTouchStart, { passive: true });
lyricsContainer.addEventListener('touchmove', handleTouchMove, { passive: false });
lyricsContainer.addEventListener('touchend', handleTouchEnd, { passive: true });
scrollEventAdded = true;
}
}
$: {
if (lyricsContainer && lyricComponents.length > 0) {
if (progress >= nextUpdate - 0.5 && !scrolling) {
console.log("computeLayout")
computeLayout();
}
if (Math.abs(lastProgress - progress) > 0.5) {
scrolling = false;
}
if (lastProgress - progress > 0) {
computeLayout();
nextUpdate = progress;
} else {
const lyricLength = originalLyrics.scripts!.length;
const currentEnd = originalLyrics.scripts![currentLyricIndex].end;
const nextStart = originalLyrics.scripts![Math.min(currentLyricIndex + 1, lyricLength - 1)].start;
if (currentEnd !== nextStart) {
nextUpdate = currentEnd;
}
else {
nextUpdate = nextStart;
}
}
}
lastProgress = progress;
}
$: {
for (let i = 0; i < lyricElements.length; i++) {
const isCurrent = i == currentLyricIndex;
const currentLyricComponent = lyricComponents[i];
currentLyricComponent.setCurrent(isCurrent);
}
}
onMount(() => {
// Initialize
if (localStorage.getItem('debugMode') == null) {
@ -223,43 +63,23 @@
debugMode = localStorage.getItem('debugMode')!.toLowerCase() === 'true';
}
});
// handle KeyDown event
function onKeyDown(e: KeyboardEvent) {
if (e.altKey && e.shiftKey && (e.metaKey || e.key === 'OS') && e.key === 'Enter') {
debugMode = !debugMode;
localStorage.setItem('debugMode', debugMode ? 'true' : 'false');
} else if (e.key === 't') {
showTranslation = !showTranslation;
localStorage.setItem('showTranslation', showTranslation ? 'true' : 'false');
computeLayout();
}
}
function lyricClick(lyricIndex: number) {
if (player === null || originalLyrics.scripts === undefined) return;
player.currentTime = originalLyrics.scripts[lyricIndex].start;
player.play();
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if debugMode}
<span class="text-lg absolute">
progress: {progress.toFixed(2)}, nextUpdate: {nextUpdate}, scrolling: {scrolling}, current: {currentLyricIndex}
</span>
{/if}
{#if originalLyrics && originalLyrics.scripts}
<div
class="absolute top-[6.5rem] md:top-36 xl:top-0 w-screen xl:w-[52vw] px-6 md:px-12
lg:px-[7.5rem] xl:left-[46vw] xl:px-[3vw] h-[calc(100vh-17rem)] xl:h-screen font-sans
text-left no-scrollbar z-[1] pt-16 overflow-hidden"
class="absolute top-[6.5rem] md:top-36 xl:top-0 w-screen xl:w-[52vw] px-6 md:px-12 lg:px-[7.5rem] xl:left-[45vw] xl:px-[3vw] h-[calc(100vh-17rem)] xl:h-screen font-sans
text-left no-scrollbar overflow-y-auto z-[1] pt-16"
bind:this={lyricsContainer}
>
{#each lyricLines as lyric, i}
<LyricLine line={lyric} index={i} bind:this={lyricComponents[i]} {debugMode} {lyricClick} />
<LyricLine
line={lyric}
index={i}
bind:this={lyricComponents[i]}
{debugMode}
initPos={lyricComponentInitPos[i]}
/>
{/each}
<div class="relative w-full h-[50rem]"></div>
</div>
{/if}

View File

@ -2,3 +2,9 @@ export interface LyricPos {
x: number;
y: number;
}
export interface LyricLayout {
pos: LyricPos;
blur: number;
scale: number;
}

View File

@ -1,4 +1,4 @@
import * as pjson from "../../../package.json";
import * as pjson from "../../package.json";
export default function getVersion(){
return pjson.version;

View File

@ -1,25 +0,0 @@
import BezierEasing from 'bezier-easing';
export function smoothScrollTo(element: HTMLElement, to: number, duration: number, timingFunction: Function) {
const start = element.scrollTop;
const change = to - start;
const startTime = performance.now();
function animateScroll(timestamp: number) {
const elapsedTime = timestamp - startTime;
const progress = Math.min(elapsedTime / duration, 1);
const easedProgress = timingFunction(progress, 0.38, 0, 0.24, 0.99);
element.scrollTop = start + change * easedProgress;
if (elapsedTime < duration) {
requestAnimationFrame(animateScroll);
}
}
requestAnimationFrame(animateScroll);
}
// Define your custom Bézier curve function
export function customBezier(progress: number, p1x: number, p1y: number, p2x: number, p2y: number) {
return BezierEasing(p1x, p1y, p2x, p2y)(progress);
}

View File

@ -1,4 +1,4 @@
import { getVelocity } from './derivative';
import { getVelocity } from "./derivative";
/** MIT License github.com/pushkine/ */
export interface SpringParams {
@ -39,7 +39,13 @@ export class Spring {
private resetSolver() {
const curV = this.getV(this.currentTime);
this.currentTime = 0;
this.currentSolver = solveSpring(this.currentPosition, curV, this.targetPosition, 0, this.params);
this.currentSolver = solveSpring(
this.currentPosition,
curV,
this.targetPosition,
0,
this.params,
);
this.getV = getVelocity(this.currentSolver);
this.getV2 = getVelocity(this.getV);
}
@ -66,7 +72,7 @@ export class Spring {
this.queueParams.time -= delta;
if (this.queueParams.time <= 0) {
this.updateParams({
...this.queueParams
...this.queueParams,
});
}
}
@ -85,13 +91,13 @@ export class Spring {
this.queueParams = {
...(this.queuePosition ?? {}),
...params,
time: delay
time: delay,
};
} else {
this.queuePosition = undefined;
this.params = {
...this.params,
...params
...params,
};
this.resetSolver();
}
@ -101,7 +107,7 @@ export class Spring {
this.queuePosition = {
...(this.queuePosition ?? {}),
position: targetPosition,
time: delay
time: delay,
};
} else {
this.queuePosition = undefined;
@ -119,7 +125,7 @@ function solveSpring(
velocity: number,
to: number,
delay: seconds = 0,
params?: Partial<SpringParams>
params?: Partial<SpringParams>,
): (t: seconds) => number {
const soft = params?.soft ?? false;
const stiffness = params?.stiffness ?? 100;
@ -136,12 +142,17 @@ function solveSpring(
};
}
const damping_frequency = Math.sqrt(4.0 * mass * stiffness - damping ** 2.0);
const leftover = (damping * delta - 2.0 * mass * velocity) / damping_frequency;
const leftover =
(damping * delta - 2.0 * mass * velocity) / damping_frequency;
const dfm = (0.5 * damping_frequency) / mass;
const dm = -(0.5 * damping) / mass;
return (t: seconds) => {
t -= delay;
if (t < 0) return from;
return to - (Math.cos(t * dfm) * delta + Math.sin(t * dfm) * leftover) * Math.E ** (t * dm);
return (
to -
(Math.cos(t * dfm) * delta + Math.sin(t * dfm) * leftover) *
Math.E ** (t * dm)
);
};
}

View File

@ -1,270 +0,0 @@
import {
alt_sc,
apply,
buildLexer,
expectEOF,
fail,
kleft,
kmid,
kright,
opt_sc,
type Parser,
rep,
rep_sc,
seq,
str,
tok,
type Token
} from 'typescript-parsec';
import type { LrcJsonData, ParsedLrc, ScriptItem, ScriptWordsItem } from '../type';
import type { IDTag } from './type';
interface ParserScriptItem {
start: number;
text: string;
words?: ScriptWordsItem[];
translation?: string;
singer?: string;
}
function convertTimeToMs({
mins,
secs,
decimals
}: {
mins?: number | string;
secs?: number | string;
decimals?: string;
}) {
let result = 0;
if (mins) {
result += Number(mins) * 60 * 1000;
}
if (secs) {
result += Number(secs) * 1000;
}
if (decimals) {
const denom = Math.pow(10, decimals.length);
result += Number(decimals) / (denom / 1000);
}
return result;
}
const digit = Array.from({ length: 10 }, (_, i) => apply(str(i.toString()), (_) => i)).reduce(
(acc, cur) => alt_sc(cur, acc),
fail('no alternatives')
);
const numStr = apply(rep_sc(digit), (r) => r.join(''));
const num = apply(numStr, (r) => parseInt(r));
const alpha = alt_sc(
Array.from({ length: 26 }, (_, i) =>
apply(str(String.fromCharCode('a'.charCodeAt(0) + i)), (_) => String.fromCharCode('a'.charCodeAt(0) + i))
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives')),
Array.from({ length: 26 }, (_, i) =>
apply(str(String.fromCharCode('A'.charCodeAt(0) + i)), (_) => String.fromCharCode('A'.charCodeAt(0) + i))
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'))
);
const alphaStr = apply(rep(alpha), (r) => r.join(''));
function spaces<K>(): Parser<K, Token<K>[]> {
return rep_sc(str(' '));
}
const unicodeStr = rep(tok('char'));
function trimmed<K, T>(p: Parser<K, Token<T>[]>): Parser<K, Token<T>[]> {
return apply(p, (r) => {
while (r.length > 0 && r[0].text.trim() === '') {
r.shift();
}
while (r.length > 0 && r[r.length - 1].text.trim() === '') {
r.pop();
}
return r;
});
}
function padded<K, T>(p: Parser<K, T>): Parser<K, T> {
return kmid(spaces(), p, spaces());
}
function anythingTyped(types: string[]) {
return types.map((t) => tok(t)).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'));
}
function lrcTimestamp<K, T>(delim: [Parser<K, Token<T>>, Parser<K, Token<T>>]) {
const innerTS = alt_sc(
apply(seq(num, str(':'), num, str('.'), numStr), (r) =>
convertTimeToMs({ mins: r[0], secs: r[2], decimals: r[4] })
),
apply(seq(num, str('.'), numStr), (r) => convertTimeToMs({ secs: r[0], decimals: r[2] })),
apply(seq(num, str(':'), num), (r) => convertTimeToMs({ mins: r[0], secs: r[2] })),
apply(num, (r) => convertTimeToMs({ secs: r }))
);
return kmid(delim[0], innerTS, delim[1]);
}
const squareTS = lrcTimestamp([tok('['), tok(']')]);
const angleTS = lrcTimestamp([tok('<'), tok('>')]);
const lrcTag = apply(
seq(
tok('['),
alphaStr,
str(':'),
tokenParserToText(trimmed(rep(anythingTyped(['char', '[', ']', '<', '>'])))),
tok(']')
),
(r) => ({
[r[1]]: r[3]
})
);
function joinTokens<T>(tokens: Token<T>[]) {
return tokens.map((t) => t.text).join('');
}
function tokenParserToText<K, T>(p: Parser<K, Token<T>> | Parser<K, Token<T>[]>): Parser<K, string> {
return apply(p, (r: Token<T> | Token<T>[]) => {
if (Array.isArray(r)) {
return joinTokens(r);
}
return r.text;
});
}
const singerIndicator = kleft(tok('char'), str(':'));
const translateParser = kright(tok('|'), unicodeStr);
function lrcLine(
wordDiv = ' ', legacy = false
): Parser<unknown, ['script_item', ParserScriptItem] | ['lrc_tag', IDTag] | ['comment', string] | ['empty', null]> {
return alt_sc(
legacy ? apply(seq(squareTS, trimmed(rep_sc(anythingTyped(['char', '[', ']', '<', '>'])))), (r) =>
['script_item', { start: r[0], text: joinTokens(r[1]) } as ParserScriptItem] // TODO: Complete this
) : apply(
seq(
squareTS,
opt_sc(padded(singerIndicator)),
rep_sc(
seq(
opt_sc(angleTS),
trimmed(rep_sc(anythingTyped(['char', '[', ']'])))
)
),
opt_sc(trimmed(translateParser))
), (r) => {
const start = r[0];
const singerPart = r[1];
const mainPart = r[2];
const translatePart = r[3];
const text = mainPart
.map((s) => joinTokens(s[1]))
.filter((s) => s.trim().length > 0)
.join(wordDiv);
const words = mainPart
.filter((s) => joinTokens(s[1]).trim().length > 0)
.map((s) => {
const wordBegin = s[0];
const word = s[1];
let ret: Partial<ScriptWordsItem> = { start: wordBegin };
if (word[0]) {
ret.beginIndex = word[0].pos.columnBegin - 1;
}
if (word[word.length - 1]) {
ret.endIndex = word[word.length - 1].pos.columnEnd;
}
return ret as ScriptWordsItem; // TODO: Complete this
});
const singer = singerPart?.text;
const translation = translatePart === undefined ? undefined : joinTokens(translatePart);
return ['script_item', { start, text, words, singer, translation } as ParserScriptItem];
}),
apply(lrcTag, (r) => ['lrc_tag', r as IDTag]),
apply(seq(spaces(), str('#'), unicodeStr), (cmt) => ['comment', cmt[2].join('')] as const),
apply(spaces(), (_) => ['empty', null] as const)
);
}
export function dumpToken<T>(t: Token<T> | undefined): string {
if (t === undefined) {
return '<EOF>';
}
return '`' + t.text + '` -> ' + dumpToken(t.next);
}
export function parseLRC(
input: string,
{ wordDiv, strict, legacy }: { wordDiv?: string; strict?: boolean; legacy?: boolean } = {}
): ParsedLrc {
const tokenizer = buildLexer([
[true, /^\[/gu, '['],
[true, /^\]/gu, ']'],
[true, /^</gu, '<'],
[true, /^>/gu, '>'],
[true, /^\|/gu, '|'],
[true, /^./gu, 'char']
]);
const lines = input
.split(/\r\n|\r|\n/gu)
.filter((line) => line.trim().length > 0)
.map((line) => tokenizer.parse(line));
return lines
.map((line) => {
const res = expectEOF(lrcLine(wordDiv, legacy).parse(line));
if (!res.successful) {
if (strict) {
throw new Error('Failed to parse full line: ' + dumpToken(line));
} else {
console.error('Failed to parse full line: ' + dumpToken(line));
}
return null;
}
return res.candidates[0].result;
})
.filter((r) => r !== null)
.reduce((acc, cur) => {
switch (cur[0]) {
case 'lrc_tag':
Object.assign(acc, cur[1]);
return acc;
case 'script_item':
acc.scripts = acc.scripts || [];
acc.scripts.push(cur[1]);
return acc;
default:
return acc;
}
}, {} as ParsedLrc);
}
export default function lrcParser(lrc: string): LrcJsonData {
const parsedLrc = parseLRC(lrc, { wordDiv: '', strict: false });
if (parsedLrc.scripts === undefined) {
return parsedLrc as LrcJsonData;
}
let finalLrc: LrcJsonData = parsedLrc as LrcJsonData;
let lyrics: ScriptItem[] = [];
let i = 0;
while (i < parsedLrc.scripts.length - 1) {
let lyricLine = parsedLrc.scripts[i] as ScriptItem;
lyricLine.start/=1000;
lyricLine.end = parsedLrc.scripts[i+1].start / 1000;
if (parsedLrc.scripts[i+1].text.trim() === "") {
i+=2;
} else i++;
if (lyricLine.text.trim() !== "") {
lyrics.push(lyricLine);
}
}
finalLrc.scripts = lyrics;
return finalLrc;
}

View File

@ -1,20 +0,0 @@
import type { LyricLine } from '@applemusic-like-lyrics/core';
import type { ScriptItem } from '$lib/lyrics/LRCparser';
export default function mapLRCtoAMLL(line: ScriptItem, i: number, lines: ScriptItem[]): LyricLine {
return {
words: [
{
word: line.text,
startTime: line.start * 1000,
endTime: line.end * 1000
}
],
startTime: line.start * 1000,
endTime: line.end * 1000,
translatedLyric: line.translation ?? "",
romanLyric: '',
isBG: false,
isDuet: false
};
}

View File

@ -1,3 +0,0 @@
export * from "./parser";
export * from "./writer";
export type * from "./ttml-types";

View File

@ -1,168 +0,0 @@
/**
* @fileoverview
* TTML
* Apple Music
* @see https://www.w3.org/TR/2018/REC-ttml1-20181108/
*/
import type {
LyricLine,
LyricWord,
TTMLLyric,
TTMLMetadata,
} from "./ttml-types";
const timeRegexp =
/^(((?<hour>[0-9]+):)?(?<min>[0-9]+):)?(?<sec>[0-9]+([.:]([0-9]+))?)/;
function parseTimespan(timeSpan: string): number {
const matches = timeRegexp.exec(timeSpan);
if (matches) {
const hour = Number(matches.groups?.hour || "0");
const min = Number(matches.groups?.min || "0");
const sec = Number(matches.groups?.sec.replace(/:/, ".") || "0");
return Math.floor((hour * 3600 + min * 60 + sec) * 1000);
}
throw new TypeError(`时间戳字符串解析失败:${timeSpan}`);
}
export function parseTTML(ttmlText: string): TTMLLyric {
const domParser = new DOMParser();
const ttmlDoc: XMLDocument = domParser.parseFromString(
ttmlText,
"application/xml",
);
let mainAgentId = "v1";
const metadata: TTMLMetadata[] = [];
for (const meta of ttmlDoc.querySelectorAll("meta")) {
if (meta.tagName === "amll:meta") {
const key = meta.getAttribute("key");
if (key) {
const value = meta.getAttribute("value");
if (value) {
const existing = metadata.find((m) => m.key === key);
if (existing) {
existing.value.push(value);
} else {
metadata.push({
key,
value: [value],
});
}
}
}
}
}
for (const agent of ttmlDoc.querySelectorAll("ttm\\:agent")) {
if (agent.getAttribute("type") === "person") {
const id = agent.getAttribute("xml:id");
if (id) {
mainAgentId = id;
}
}
}
const lyricLines: LyricLine[] = [];
function parseParseLine(lineEl: Element, isBG = false, isDuet = false) {
const line: LyricLine = {
words: [],
translatedLyric: "",
romanLyric: "",
isBG,
isDuet:
!!lineEl.getAttribute("ttm:agent") &&
lineEl.getAttribute("ttm:agent") !== mainAgentId,
startTime: 0,
endTime: 0,
};
if (isBG) line.isDuet = isDuet;
let haveBg = false;
for (const wordNode of lineEl.childNodes) {
if (wordNode.nodeType === Node.TEXT_NODE) {
line.words?.push({
word: wordNode.textContent ?? "",
startTime: 0,
endTime: 0,
});
} else if (wordNode.nodeType === Node.ELEMENT_NODE) {
const wordEl = wordNode as Element;
const role = wordEl.getAttribute("ttm:role");
if (wordEl.nodeName === "span" && role) {
if (role === "x-bg") {
parseParseLine(wordEl, true, line.isDuet);
haveBg = true;
} else if (role === "x-translation") {
line.translatedLyric = wordEl.innerHTML;
} else if (role === "x-roman") {
line.romanLyric = wordEl.innerHTML;
}
} else if (wordEl.hasAttribute("begin") && wordEl.hasAttribute("end")) {
const word: LyricWord = {
word: wordNode.textContent ?? "",
startTime: parseTimespan(wordEl.getAttribute("begin") ?? ""),
endTime: parseTimespan(wordEl.getAttribute("end") ?? ""),
};
const emptyBeat = wordEl.getAttribute("amll:empty-beat");
if (emptyBeat) {
word.emptyBeat = Number(emptyBeat);
}
line.words.push(word);
}
}
}
if (line.isBG) {
const firstWord = line.words?.[0];
if (firstWord?.word.startsWith("(")) {
firstWord.word = firstWord.word.substring(1);
if (firstWord.word.length === 0) {
line.words.shift();
}
}
const lastWord = line.words?.[line.words.length - 1];
if (lastWord?.word.endsWith(")")) {
lastWord.word = lastWord.word.substring(0, lastWord.word.length - 1);
if (lastWord.word.length === 0) {
line.words.pop();
}
}
}
const startTime = lineEl.getAttribute("begin");
const endTime = lineEl.getAttribute("end");
if (startTime && endTime) {
line.startTime = parseTimespan(startTime);
line.endTime = parseTimespan(endTime);
} else {
line.startTime = line.words
.filter((v) => v.word.trim().length > 0)
.reduce((pv, cv) => Math.min(pv, cv.startTime), Infinity);
line.endTime = line.words
.filter((v) => v.word.trim().length > 0)
.reduce((pv, cv) => Math.max(pv, cv.endTime), 0);
}
if (haveBg) {
const bgLine = lyricLines.pop();
lyricLines.push(line);
if (bgLine) lyricLines.push(bgLine);
} else {
lyricLines.push(line);
}
}
for (const lineEl of ttmlDoc.querySelectorAll("body p[begin][end]")) {
parseParseLine(lineEl);
}
return {
metadata,
lyricLines: lyricLines,
};
}

View File

@ -1,26 +0,0 @@
export interface TTMLMetadata {
key: string;
value: string[];
}
export interface TTMLLyric {
metadata: TTMLMetadata[];
lyricLines: LyricLine[];
}
export interface LyricWord {
startTime: number;
endTime: number;
word: string;
emptyBeat?: number;
}
export interface LyricLine {
words: LyricWord[];
translatedLyric: string;
romanLyric: string;
isBG: boolean;
isDuet: boolean;
startTime: number;
endTime: number;
}

View File

@ -1,260 +0,0 @@
/**
* @fileoverview
* TTML
*
*/
import type { LyricLine, LyricWord, TTMLLyric } from "./ttml-types";
function msToTimestamp(timeMS: number): string {
let time = timeMS;
if (!Number.isSafeInteger(time) || time < 0) {
return "00:00.000";
}
if (time === Infinity) {
return "99:99.999";
}
time = time / 1000;
const secs = time % 60;
time = (time - secs) / 60;
const mins = time % 60;
const hrs = (time - mins) / 60;
const h = hrs.toString().padStart(2, "0");
const m = mins.toString().padStart(2, "0");
const s = secs.toFixed(3).padStart(6, "0");
if (hrs > 0) {
return `${h}:${m}:${s}`;
}
return `${m}:${s}`;
}
export function exportTTML(ttmlLyric: TTMLLyric, pretty = false): string {
const params: LyricLine[][] = [];
const lyric = ttmlLyric.lyricLines;
let tmp: LyricLine[] = [];
for (const line of lyric) {
if (line.words.length === 0 && tmp.length > 0) {
params.push(tmp);
tmp = [];
} else {
tmp.push(line);
}
}
if (tmp.length > 0) {
params.push(tmp);
}
const doc = new Document();
function createWordElement(word: LyricWord): Element {
const span = doc.createElement("span");
span.setAttribute("begin", msToTimestamp(word.startTime));
span.setAttribute("end", msToTimestamp(word.endTime));
if (word.emptyBeat) {
span.setAttribute("amll:empty-beat", `${word.emptyBeat}`);
}
span.appendChild(doc.createTextNode(word.word));
return span;
}
const ttRoot = doc.createElement("tt");
ttRoot.setAttribute("xmlns", "http://www.w3.org/ns/ttml");
ttRoot.setAttribute("xmlns:ttm", "http://www.w3.org/ns/ttml#metadata");
ttRoot.setAttribute("xmlns:amll", "http://www.example.com/ns/amll");
ttRoot.setAttribute(
"xmlns:itunes",
"http://music.apple.com/lyric-ttml-internal",
);
doc.appendChild(ttRoot);
const head = doc.createElement("head");
ttRoot.appendChild(head);
const body = doc.createElement("body");
const hasOtherPerson = !!lyric.find((v) => v.isDuet);
const metadataEl = doc.createElement("metadata");
const mainPersonAgent = doc.createElement("ttm:agent");
mainPersonAgent.setAttribute("type", "person");
mainPersonAgent.setAttribute("xml:id", "v1");
metadataEl.appendChild(mainPersonAgent);
if (hasOtherPerson) {
const otherPersonAgent = doc.createElement("ttm:agent");
otherPersonAgent.setAttribute("type", "other");
otherPersonAgent.setAttribute("xml:id", "v2");
metadataEl.appendChild(otherPersonAgent);
}
for (const metadata of ttmlLyric.metadata) {
for (const value of metadata.value) {
const metaEl = doc.createElement("amll:meta");
metaEl.setAttribute("key", metadata.key);
metaEl.setAttribute("value", value);
metadataEl.appendChild(metaEl);
}
}
head.appendChild(metadataEl);
let i = 0;
const guessDuration = lyric[lyric.length - 1]?.endTime ?? 0;
body.setAttribute("dur", msToTimestamp(guessDuration));
for (const param of params) {
const paramDiv = doc.createElement("div");
const beginTime = param[0]?.startTime ?? 0;
const endTime = param[param.length - 1]?.endTime ?? 0;
paramDiv.setAttribute("begin", msToTimestamp(beginTime));
paramDiv.setAttribute("end", msToTimestamp(endTime));
for (let lineIndex = 0; lineIndex < param.length; lineIndex++) {
const line = param[lineIndex];
const lineP = doc.createElement("p");
const beginTime = line.startTime ?? 0;
const endTime = line.endTime;
lineP.setAttribute("begin", msToTimestamp(beginTime));
lineP.setAttribute("end", msToTimestamp(endTime));
lineP.setAttribute("ttm:agent", line.isDuet ? "v2" : "v1");
lineP.setAttribute("itunes:key", `L${++i}`);
if (line.words.length > 1) {
let beginTime = Infinity;
let endTime = 0;
for (const word of line.words) {
if (word.word.trim().length === 0) {
lineP.appendChild(doc.createTextNode(word.word));
} else {
const span = createWordElement(word);
lineP.appendChild(span);
beginTime = Math.min(beginTime, word.startTime);
endTime = Math.max(endTime, word.endTime);
}
}
lineP.setAttribute("begin", msToTimestamp(line.startTime));
lineP.setAttribute("end", msToTimestamp(line.endTime));
} else if (line.words.length === 1) {
const word = line.words[0];
lineP.appendChild(doc.createTextNode(word.word));
lineP.setAttribute("begin", msToTimestamp(word.startTime));
lineP.setAttribute("end", msToTimestamp(word.endTime));
}
const nextLine = param[lineIndex + 1];
if (nextLine?.isBG) {
lineIndex++;
const bgLine = nextLine;
const bgLineSpan = doc.createElement("span");
bgLineSpan.setAttribute("ttm:role", "x-bg");
if (bgLine.words.length > 1) {
let beginTime = Infinity;
let endTime = 0;
for (
let wordIndex = 0;
wordIndex < bgLine.words.length;
wordIndex++
) {
const word = bgLine.words[wordIndex];
if (word.word.trim().length === 0) {
bgLineSpan.appendChild(doc.createTextNode(word.word));
} else {
const span = createWordElement(word);
if (wordIndex === 0) {
span.prepend(doc.createTextNode("("));
} else if (wordIndex === bgLine.words.length - 1) {
span.appendChild(doc.createTextNode(")"));
}
bgLineSpan.appendChild(span);
beginTime = Math.min(beginTime, word.startTime);
endTime = Math.max(endTime, word.endTime);
}
}
bgLineSpan.setAttribute("begin", msToTimestamp(beginTime));
bgLineSpan.setAttribute("end", msToTimestamp(endTime));
} else if (bgLine.words.length === 1) {
const word = bgLine.words[0];
bgLineSpan.appendChild(doc.createTextNode(`(${word.word})`));
bgLineSpan.setAttribute("begin", msToTimestamp(word.startTime));
bgLineSpan.setAttribute("end", msToTimestamp(word.endTime));
}
if (bgLine.translatedLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-translation");
span.setAttribute("xml:lang", "zh-CN");
span.appendChild(doc.createTextNode(bgLine.translatedLyric));
bgLineSpan.appendChild(span);
}
if (bgLine.romanLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-roman");
span.appendChild(doc.createTextNode(bgLine.romanLyric));
bgLineSpan.appendChild(span);
}
lineP.appendChild(bgLineSpan);
}
if (line.translatedLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-translation");
span.setAttribute("xml:lang", "zh-CN");
span.appendChild(doc.createTextNode(line.translatedLyric));
lineP.appendChild(span);
}
if (line.romanLyric) {
const span = doc.createElement("span");
span.setAttribute("ttm:role", "x-roman");
span.appendChild(doc.createTextNode(line.romanLyric));
lineP.appendChild(span);
}
paramDiv.appendChild(lineP);
}
body.appendChild(paramDiv);
}
ttRoot.appendChild(body);
if (pretty) {
const xsltDoc = new DOMParser().parseFromString(
[
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">',
' <xsl:strip-space elements="*"/>',
' <xsl:template match="para[content-style][not(text())]">',
' <xsl:value-of select="normalize-space(.)"/>',
" </xsl:template>",
' <xsl:template match="node()|@*">',
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
" </xsl:template>",
' <xsl:output indent="yes"/>',
"</xsl:stylesheet>",
].join("\n"),
"application/xml",
);
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsltDoc);
const resultDoc = xsltProcessor.transformToDocument(doc);
return new XMLSerializer().serializeToString(resultDoc);
}
return new XMLSerializer().serializeToString(doc);
}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import extractFileName from '$lib/utils/extractFileName';
import getVersion from '$lib/utils/getVersion';
import toHumanSize from '$lib/utils/humanSize';
import localforage from '$lib/utils/storage';
import extractFileName from '$lib/extractFileName';
import getVersion from '$lib/getVersion';
import toHumanSize from '$lib/humanSize';
import localforage from '$lib/storage';
interface Song {
name: string;
singer?: string;

View File

@ -1,5 +1,5 @@
import { safePath } from '$lib/server/safePath';
import { getCurrentFormattedDateTime } from '$lib/utils/songUpdateTime';
import { getCurrentFormattedDateTime } from '$lib/songUpdateTime';
import { json, error } from '@sveltejs/kit';
import fs from 'fs';
import type { RequestHandler } from './$types';

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { getCurrentFormattedDateTime } from '$lib/utils/songUpdateTime';
import { getCurrentFormattedDateTime } from '$lib/songUpdateTime';
let templateSongData: MusicMetadata = {
id: '',
name: '',

View File

@ -2,7 +2,7 @@
import { page } from '$app/stores';
import FileList from '$lib/components/import/fileList.svelte';
import FileSelector from '$lib/components/import/fileSelector.svelte';
import localforage from '$lib/utils/storage';
import localforage from '$lib/storage';
import { fileListState } from '$lib/state/fileList.state';
import { useAtom } from 'jotai-svelte';
const fileList = useAtom(fileListState);

View File

@ -4,7 +4,7 @@
import { fileListState, finalFileListState } from '$lib/state/fileList.state';
import { localImportFailed, localImportSuccess } from '$lib/state/localImportStatus.state';
import { useAtom } from 'jotai-svelte';
import localforage from '$lib/utils/storage';
import localforage from '$lib/storage';
import { v1 as uuidv1 } from 'uuid';
const fileList = useAtom(fileListState);
const finalFiles = useAtom(finalFileListState);

View File

@ -4,7 +4,8 @@
import Background from '$lib/components/background.svelte';
import Cover from '$lib/components/cover.svelte';
import InteractiveBox from '$lib/components/interactiveBox.svelte';
import extractFileName from '$lib/utils/extractFileName';
import Lyrics from '$lib/components/lyrics/lyrics.svelte';
import extractFileName from '$lib/extractFileName';
import localforage from 'localforage';
import { writable } from 'svelte/store';
import lrcParser from '$lib/lyrics/lrc/parser';
@ -213,7 +214,7 @@
<NewLyrics {originalLyrics} progress={currentProgress} player={audioPlayer}/>
<!-- <Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} player={audioPlayer} class="hidden" /> -->
<Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} player={audioPlayer} class="hidden" />
<audio
bind:this={audioPlayer}

View File

@ -2,10 +2,9 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import rollupNodePolyFill from 'rollup-plugin-node-polyfills';
import wasm from 'vite-plugin-wasm';
export default defineConfig({
plugins: [sveltekit(), wasm()],
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
},
@ -28,7 +27,7 @@ export default defineConfig({
},
server: {
fs: {
allow: ['./package.json']
allow: ["./package.json"]
}
}
});