merge: update from dev branch

This commit is contained in:
alikia2x 2024-07-25 02:35:19 +08:00
parent 74d783b5d5
commit ab8c32ffb8
18 changed files with 433 additions and 243 deletions

View File

@ -1,4 +0,0 @@
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

12
.idea/AquaVox.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,48 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/AquaVox.iml" filepath="$PROJECT_DIR$/.idea/AquaVox.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev Server" type="ShConfigurationType" focusToolWindowBeforeRun="true">
<option name="SCRIPT_TEXT" value="bun dev" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/opt/homebrew/bin/nu" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,6 +1,6 @@
{ {
"name": "aquavox", "name": "aquavox",
"version": "1.12.8", "version": "1.12.12",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View File

@ -1,3 +0,0 @@
<div>
</div>

View File

@ -35,11 +35,11 @@
} }
function volumeBarChangeTouch(e: TouchEvent) { function volumeBarChangeTouch(e: TouchEvent) {
const value = turncate( const value = truncate(
e.touches[0].clientX - volumeBar.getBoundingClientRect().x, e.touches[0].clientX - volumeBar.getBoundingClientRect().x,
0, 0,
volumeBar.getBoundingClientRect().width volumeBar.getBoundingClientRect().width
) / volumeBar.getBoundingClientRect().width; ) / volumeBar.getBoundingClientRect().width;
adjustVolume(value); adjustVolume(value);
localStorage.setItem('volume', value.toString()); localStorage.setItem('volume', value.toString());
} }
@ -49,10 +49,14 @@
progressBarSlideValue.set((e.offsetX / progressBar.getBoundingClientRect().width) * duration); progressBarSlideValue.set((e.offsetX / progressBar.getBoundingClientRect().width) * duration);
} }
function turncate(value: number, min: number, max: number) { function truncate(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max); return Math.min(Math.max(value, min), max);
} }
function progressBarMouseUp(offsetX: number) {
adjustDisplayProgress(offsetX / progressBar.getBoundingClientRect().width);
}
onMount(() => { onMount(() => {
mql.addEventListener('change', (e) => { mql.addEventListener('change', (e) => {
showInfoTop = e.matches && hasLyrics; showInfoTop = e.matches && hasLyrics;
@ -85,7 +89,7 @@
> >
{#if !showInfoTop} {#if !showInfoTop}
<div class="song-info"> <div class="song-info">
<div class="song-info-top {isInfoTopOverflowing ? 'animate' : ''}" bind:this={songInfoTopContainer}> <div class="song-info-regular {isInfoTopOverflowing ? 'animate' : ''}" bind:this={songInfoTopContainer}>
<span <span
class="song-name text-shadow {isInfoTopOverflowing ? 'animate' : ''}" class="song-name text-shadow {isInfoTopOverflowing ? 'animate' : ''}"
bind:this={songInfoTopContent}>{name}</span bind:this={songInfoTopContent}>{name}</span
@ -100,51 +104,34 @@
{formatDuration(progress)} {formatDuration(progress)}
</div> </div>
<div <div
class="progress-bar shadow-md" aria-valuemax={duration}
on:click={(e) => progressBarOnClick(e)} aria-valuemin="0"
aria-valuenow={progress}
bind:this={progressBar} bind:this={progressBar}
class="progress-bar shadow-md"
on:keydown
on:keyup
on:mousedown={() => { on:mousedown={() => {
userAdjustingProgress.set(true); userAdjustingProgress.set(true);
}} }}
on:mousemove={(e) => { on:mousemove={(e) => {
if ($userAdjustingProgress) { if ($userAdjustingProgress) {
console.log(e.offsetX )
adjustDisplayProgress(e.offsetX / progressBar.getBoundingClientRect().width); adjustDisplayProgress(e.offsetX / progressBar.getBoundingClientRect().width);
} }
}} }}
on:touchstart={(e) => { on:mouseup={(e) => {
if (e.cancelable) { const offsetX = e.offsetX;
e.preventDefault(); progressBarOnClick(e);
} // Q: why it needs delay?
userAdjustingProgress.set(true); // A: I do not know.
}} setTimeout(()=> {
on:touchmove={(e) => { userAdjustingProgress.set(false);
e.preventDefault(); progressBarMouseUp(offsetX);
userAdjustingProgress.set(true); }, 50);
if ($userAdjustingProgress) {
lastTouchProgress =
turncate(
e.touches[0].clientX - progressBar.getBoundingClientRect().x,
0,
progressBar.getBoundingClientRect().width
) / progressBar.getBoundingClientRect().width;
adjustDisplayProgress(lastTouchProgress);
}
}}
on:touchend={(e) => {
e.preventDefault();
userAdjustingProgress.set(false);
adjustProgress(lastTouchProgress);
}}
on:mouseup={() => {
userAdjustingProgress.set(false);
}} }}
role="slider" role="slider"
aria-valuemin="0"
aria-valuemax={duration}
aria-valuenow={progress}
tabindex="0" tabindex="0"
on:keydown
on:keyup
> >
<div class="bar" style={`width: ${(progress / (duration + 0.001)) * 100}%;`}></div> <div class="bar" style={`width: ${(progress / (duration + 0.001)) * 100}%;`}></div>
</div> </div>
@ -152,26 +139,49 @@
<div class="time-indicator text-shadow-md time-total">{formatDuration(duration)}</div> <div class="time-indicator text-shadow-md time-total">{formatDuration(duration)}</div>
</div> </div>
<div class="controls top-32 flex h-16 overflow-hidden items-center justify-center"> <div class="controls top-32 flex h-16 overflow-hidden items-center justify-center">
<button style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );" class="control-btn previous"> <button class="control-btn previous" style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );">
<img class="control-img switch-song-img" src="/previous.svg" alt="上一曲" /> <img alt="上一曲" class="control-img switch-song-img" src="/previous.svg" />
</button> </button>
<button <button
style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );"
class="control-btn play-btn" class="control-btn play-btn"
on:click={() => clickPlay()} on:click={(e) => clickPlay()}
on:focus={null}
on:mouseleave={(e) => {
e.currentTarget.style.backgroundColor = '';
}}
on:mouseover={(e) => {
e.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
}}
on:touchend={(e) => {
e.preventDefault();
e.currentTarget.style.backgroundColor = '';
e.currentTarget.style.scale = '1';
clickPlay();
}}
on:touchstart={(e) => {
e.preventDefault();
e.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
e.currentTarget.style.scale = '0.8';
}}
style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );"
> >
<img class="control-img" src={paused ? '/play.svg' : '/pause.svg'} alt="暂停或播放" /> <img alt={paused ? '播放' : '暂停'} class="control-img" src={paused ? '/play.svg' : '/pause.svg'} />
</button> </button>
<button style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );" class="control-btn next"> <button class="control-btn next" style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );">
<img class="control-img switch-song-img" src="/next.svg" alt="下一曲" /> <img alt="下一曲" class="control-img switch-song-img" src="/next.svg" />
</button> </button>
</div> </div>
<div class="relative top-52 h-6 flex"> <div class="relative top-52 h-6 flex">
<img class="scale-75" src="/volumeDown.svg" alt="最小音量" /> <img alt="最小音量" class="scale-75" src="/volumeDown.svg" />
<div <div
aria-valuemax="1"
aria-valuemin="0"
aria-valuenow={volume}
bind:this={volumeBar}
class="progress-bar shadow-md !top-1/2 !translate-y-[-50%]" class="progress-bar shadow-md !top-1/2 !translate-y-[-50%]"
on:click={(e) => volumeBarOnChange(e)} on:click={(e) => volumeBarOnChange(e)}
bind:this={volumeBar} on:keydown
on:keyup
on:mousedown={() => { on:mousedown={() => {
userAdjustingVolume = true; userAdjustingVolume = true;
}} }}
@ -180,11 +190,12 @@
volumeBarOnChange(e); volumeBarOnChange(e);
} }
}} }}
on:touchstart={(e) => { on:mouseup={() => {
if (e.cancelable) { userAdjustingVolume = false;
e.preventDefault(); }}
} on:touchend={(e) => {
userAdjustingVolume = true; e.preventDefault();
userAdjustingVolume = false;
}} }}
on:touchmove={(e) => { on:touchmove={(e) => {
e.preventDefault(); e.preventDefault();
@ -193,24 +204,18 @@
volumeBarChangeTouch(e); volumeBarChangeTouch(e);
} }
}} }}
on:touchend={(e) => { on:touchstart={(e) => {
e.preventDefault(); if (e.cancelable) {
userAdjustingVolume = false; e.preventDefault();
}} }
on:mouseup={() => { userAdjustingVolume = true;
userAdjustingVolume = false;
}} }}
role="slider" role="slider"
aria-valuemin="0"
aria-valuemax="1"
aria-valuenow={volume}
tabindex="0" tabindex="0"
on:keydown
on:keyup
> >
<div class="bar" style={`width: ${volume * 100}%;`}></div> <div class="bar" style={`width: ${volume * 100}%;`}></div>
</div> </div>
<img class="scale-75" src="/volumeUp.svg" alt="最大音量" /> <img alt="最大音量" class="scale-75" src="/volumeUp.svg" />
</div> </div>
</div> </div>
@ -221,6 +226,7 @@
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
} }
.control-btn { .control-btn {
display: inline-block; display: inline-block;
height: 3.7rem; height: 3.7rem;
@ -228,11 +234,10 @@
cursor: pointer; cursor: pointer;
margin: 0 0.5rem; margin: 0 0.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
transition: 0.1s; transition: 0.45s;
} scale: 1;
.control-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
} }
.control-img { .control-img {
height: 2rem; height: 2rem;
width: 2rem; width: 2rem;
@ -240,6 +245,7 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
.switch-song-img { .switch-song-img {
width: auto !important; width: auto !important;
height: 1.7rem !important; height: 1.7rem !important;
@ -256,19 +262,21 @@
font-family: sans-serif; font-family: sans-serif;
text-align: center; text-align: center;
} }
.song-info-top {
.song-info-regular {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
height: 2.375rem;
} }
.song-info-top.animate { .song-info-regular.animate {
mask-image: linear-gradient( mask-image: linear-gradient(
90deg, 90deg,
rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 2rem, rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 5rem), rgba(0, 0, 0, 1) calc(100% - 5rem),
rgba(0, 0, 0, 0) 100% rgba(0, 0, 0, 0) 100%
); );
} }
@ -283,12 +291,15 @@
height: 2.5rem; height: 2.5rem;
display: inline-block; display: inline-block;
} }
.song-name.animate { .song-name.animate {
animation: scroll 10s linear infinite; animation: scroll 10s linear infinite;
} }
.song-name::-webkit-scrollbar { .song-name::-webkit-scrollbar {
display: none; display: none;
} }
@keyframes scroll { @keyframes scroll {
0% { 0% {
transform: translateX(100%); transform: translateX(100%);
@ -300,10 +311,12 @@
transform: translateX(-100%); transform: translateX(-100%);
} }
} }
.song-author { .song-author {
font-size: 1.2rem; font-size: 1.2rem;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
.progress { .progress {
position: absolute; position: absolute;
width: 100%; width: 100%;
@ -311,6 +324,7 @@
transform: translate(-50%, 0); transform: translate(-50%, 0);
height: 2.4rem; height: 2.4rem;
} }
.progress-bar { .progress-bar {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
@ -325,9 +339,11 @@
cursor: pointer; cursor: pointer;
transition: 0.3s; transition: 0.3s;
} }
.progress-bar:hover { .progress-bar:hover {
height: 0.7rem; height: 0.7rem;
} }
.bar { .bar {
background-color: white; background-color: white;
position: absolute; position: absolute;
@ -351,10 +367,18 @@
display: inline-block; display: inline-block;
top: 0.2rem; top: 0.2rem;
} }
.time-current { .time-current {
left: 0; left: 0;
} }
.time-total { .time-total {
right: 0; right: 0;
} }
@media (min-width: 768px) {
.control-btn {
transition: 0.1s
}
}
</style> </style>

View File

@ -4,6 +4,7 @@
import progressBarRaw from '$lib/state/progressBarRaw'; import progressBarRaw from '$lib/state/progressBarRaw';
import type { LrcJsonData } from 'lrc-parser-ts'; import type { LrcJsonData } from 'lrc-parser-ts';
import progressBarSlideValue from '$lib/state/progressBarSlideValue'; import progressBarSlideValue from '$lib/state/progressBarSlideValue';
import nextUpdate from '$lib/state/nextUpdate';
// Component input properties // Component input properties
export let lyrics: string[]; export let lyrics: string[];
@ -12,18 +13,25 @@
// Local state and variables // Local state and variables
let getLyricIndex: Function; let getLyricIndex: Function;
const debugMode = false; let debugMode = false;
if (localStorage.getItem('debugMode') == null) {
localStorage.setItem('debugMode', 'false');
}
else {
debugMode = localStorage.getItem('debugMode')!.toLowerCase() === "true";
}
let currentLyricIndex = -1; let currentLyricIndex = -1;
let currentPositionIndex = -1; let currentPositionIndex = -1;
let currentAnimationIndex = -1; let currentAnimationIndex = -1;
let lyricsContainer: HTMLDivElement; let lyricsContainer: HTMLDivElement | null;
let nextUpdate = -1;
let lastAdjustProgress = 0; let lastAdjustProgress = 0;
let localProgress = 0; let localProgress = 0;
let lastScroll = 0; let lastScroll = 0;
let scrolling = false; let scrolling = false;
let scriptScrolling = false; let scriptScrolling = false;
let currentLyricTopMargin = 288;
// References to lyric elements // References to lyric elements
let refs: HTMLParagraphElement[] = []; let refs: HTMLParagraphElement[] = [];
let _refs: any[] = []; let _refs: any[] = [];
@ -38,73 +46,37 @@
else return 'previous-lyric'; else return 'previous-lyric';
} }
// Scroll to corresponding lyric while adjusting progress
$: {
if ($userAdjustingProgress == true) {
const currentLyric = refs[getLyricIndex(progress)];
scrollToLyric(currentLyric);
}
}
// Update the current lyric and apply blur effect based on the progress
$: {
(() => {
if (!lyricsContainer || !originalLyrics.scripts) return;
const scripts = originalLyrics.scripts;
currentPositionIndex = getLyricIndex(progress);
const cl = scripts[currentPositionIndex];
if (cl.start <= progress && progress <= cl.end) {
currentLyricIndex = currentPositionIndex;
nextUpdate = cl.end;
} else {
currentLyricIndex = -1;
nextUpdate = cl.start;
}
const currentLyric = refs[currentPositionIndex];
if ($userAdjustingProgress || scrolling || currentLyric.getBoundingClientRect().top < 0) return;
for (let i = 0; i < scripts.length; i++) {
const offset = Math.abs(i - currentPositionIndex);
const blurRadius = Math.min(offset * 1, 16);
if (refs[i]) {
refs[i].style.filter = `blur(${blurRadius}px)`;
}
}
})();
}
// Utility function to create a sleep/delay
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Function to move the lyrics up smoothly // Function to move the lyrics up smoothly
async function moveToNextLine(h: number) { async function moveToNextLine(h: number) {
let pos = currentPositionIndex + 2; console.debug(new Date().getTime() , 'moveToNextLine', h);
for (let i = currentPositionIndex + 2; i < refs.length; i++) { // the line that's going to process (like a pointer)
// by default, it's "the next line" after the lift
let processingLineIndex = currentPositionIndex + 2;
// modify translateY of all lines in viewport one by one to lift them up
for (let i = processingLineIndex; i < refs.length; i++) {
const lyric = refs[i]; const lyric = refs[i];
lyric.style.transition = lyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease'; `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease,
font-size 200ms ease, scale 250ms ease`;
lyric.style.transform = `translateY(${-h}px)`; lyric.style.transform = `translateY(${-h}px)`;
pos = i; processingLineIndex = i;
await sleep(75); await sleep(75);
if (refs[i - 2].getBoundingClientRect().top > lyricsContainer.getBoundingClientRect().height) break; const twoLinesAhead = refs[i - 2];
if (lyricsContainer && twoLinesAhead.getBoundingClientRect().top > lyricsContainer.getBoundingClientRect().height) break;
} }
if (refs.length - pos < 3) { if (refs.length - processingLineIndex < 3) {
for (let i = pos; i < refs.length; i++) { for (let i = processingLineIndex; i < refs.length; i++) {
const lyric = refs[i]; const lyric = refs[i];
lyric.style.transition = lyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease'; 'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
lyric.style.transform = `translateY(${-h}px)`; lyric.style.transform = `translateY(${-h}px)`;
pos = i; processingLineIndex = i;
await sleep(75); await sleep(75);
} }
} else { } else {
for (let i = pos; i < refs.length; i++) { for (let i = processingLineIndex; i < refs.length; i++) {
refs[i].style.transition = refs[i].style.transition =
'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease'; 'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
const height = refs[i].getBoundingClientRect().height; const height = refs[i].getBoundingClientRect().height;
@ -112,37 +84,45 @@
} }
} }
// wait until the animation end
await sleep(650); await sleep(650);
// clear the transition to let the following style changes could be done without animation
for (let i = 0; i < refs.length; i++) { for (let i = 0; i < refs.length; i++) {
refs[i].style.transition = refs[i].style.transition =
'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease'; 'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
} }
// reset the translateY, and immediately scroll down to provide visual stability
for (let i = 0; i < refs.length; i++) { for (let i = 0; i < refs.length; i++) {
refs[i].style.transform = `translateY(0px)`; refs[i].style.transform = `translateY(0px)`;
} }
scriptScrolling = true; scriptScrolling = true;
lyricsContainer.scrollTop += h; if (lyricsContainer !== null) {
setTimeout(() => { lyricsContainer.scrollTop += h;
scriptScrolling = false; }
}, 500); await sleep(500);
scriptScrolling = false;
} }
// Scroll the lyrics container to the given lyric // Scroll the lyrics container to the given lyric
async function scrollToLyric(currentLyric: HTMLParagraphElement) { async function scrollToLyric(currentLyric: HTMLParagraphElement) {
if (!originalLyrics || !originalLyrics.scripts || !lyricsContainer) return; if (!originalLyrics || !originalLyrics.scripts || !lyricsContainer) return;
scriptScrolling = true; scriptScrolling = true;
lyricsContainer.scrollTop += currentLyric.getBoundingClientRect().top - 144; lyricsContainer.scrollTop += currentLyric.getBoundingClientRect().top - currentLyricTopMargin;
for (let i = 0; i < refs.length; i++) {
refs[i].style.transform = 'translateY(0px)';
}
setTimeout(() => { setTimeout(() => {
scriptScrolling = false; scriptScrolling = false;
}, 500); }, 500);
} }
// Handle user adjusting progress state changes // Handle user adjusting progress state changes
userAdjustingProgress.subscribe((v) => { userAdjustingProgress.subscribe((adjusting) => {
if (!originalLyrics) return; if (!originalLyrics) return;
const scripts = originalLyrics.scripts; const scripts = originalLyrics.scripts;
if (!scripts) return; if (!scripts) return;
if (v) { if (adjusting) {
for (let i = 0; i < scripts.length; i++) { for (let i = 0; i < scripts.length; i++) {
refs[i].style.filter = `blur(0px)`; refs[i].style.filter = `blur(0px)`;
} }
@ -193,73 +173,132 @@
}, 5500); }, 5500);
} }
// Update lyrics position based on progress // Utility function to create a sleep/delay
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Scroll to corresponding lyric while adjusting progress
$: {
if ($userAdjustingProgress == true) {
const currentLyric = refs[getLyricIndex(progress)];
scrollToLyric(currentLyric);
}
}
// Update the current lyric and apply blur effect based on the progress
$: { $: {
(() => { (() => {
if ($userAdjustingProgress) { if (!lyricsContainer || !originalLyrics.scripts) return;
nextUpdate = progress;
return;
}
if (nextUpdate - progress >= 0.05) return; const scripts = originalLyrics.scripts;
if ( currentPositionIndex = getLyricIndex(progress);
currentPositionIndex < 0 || const cl = scripts[currentPositionIndex];
currentPositionIndex === currentAnimationIndex ||
currentPositionIndex === lastAdjustProgress || if (cl.start <= progress && progress <= cl.end) {
scrolling currentLyricIndex = currentPositionIndex;
) nextUpdate.set(cl.end);
return; } else {
currentLyricIndex = -1;
nextUpdate.set(cl.start);
}
const currentLyric = refs[currentPositionIndex]; const currentLyric = refs[currentPositionIndex];
if ($userAdjustingProgress || scrolling || currentLyric.getBoundingClientRect().top < 0) return;
if (originalLyrics.scripts && currentLyric.getBoundingClientRect().top < 0) return; for (let i = 0; i < scripts.length; i++) {
const offset = Math.abs(i - currentPositionIndex);
const offsetHeight = const blurRadius = Math.min(offset * 0.96, 16);
refs[currentPositionIndex].getBoundingClientRect().height + if (refs[i]) {
refs[currentPositionIndex].getBoundingClientRect().top - refs[i].style.filter = `blur(${blurRadius}px)`;
144; }
currentLyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
currentLyric.style.transform = `translateY(${-offsetHeight}px)`;
for (let i = currentPositionIndex - 1; i >= 0; i--) {
refs[i].style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
const height = refs[i].getBoundingClientRect().height;
refs[i].style.transform = `translateY(${-offsetHeight}px)`;
} }
if (currentPositionIndex + 1 < refs.length) {
const nextLyric = refs[currentPositionIndex + 1];
nextLyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
nextLyric.style.transform = `translateY(${-offsetHeight}px)`;
moveToNextLine(offsetHeight);
}
currentAnimationIndex = currentPositionIndex;
})(); })();
} }
nextUpdate.subscribe(async (nextUpdate) => {
if (
currentPositionIndex < 0 ||
currentPositionIndex === currentAnimationIndex ||
currentPositionIndex === lastAdjustProgress ||
$userAdjustingProgress === true ||
scrolling
) return;
const currentLyric = refs[currentPositionIndex];
if (originalLyrics.scripts && currentLyric.getBoundingClientRect().top < 0) return;
const offsetHeight =
refs[currentPositionIndex].getBoundingClientRect().top -
currentLyricTopMargin;
// prepare current line
currentLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
currentLyric.style.transform = `translateY(${-offsetHeight}px)`;
for (let i = currentPositionIndex - 1; i >= 0; i--) {
refs[i].style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
refs[i].style.transform = `translateY(${-offsetHeight}px)`;
}
if (currentPositionIndex + 1 < refs.length) {
const nextLyric = refs[currentPositionIndex + 1];
nextLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
nextLyric.style.transform = `translateY(${-offsetHeight}px)`;
await moveToNextLine(offsetHeight);
}
currentAnimationIndex = currentPositionIndex;
})
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');
}
}
function extractTranslateValue(s: string): string | null {
const regex = /translateY\((-?\d*px)\)/;
let arr = regex.exec(s);
return arr==null ? null : arr[1];
}
</script> </script>
{#if lyrics && originalLyrics} <svelte:window on:keydown={onKeyDown} />
{#if debugMode && lyricsContainer}
<div
class="absolute top-6 right-10 font-mono text-sm backdrop-blur-md z-20 bg-[rgba(255,255,255,0.15)] px-2 rounded-xl">
<p>
LyricIndex: {currentLyricIndex} PositionIndex: {currentPositionIndex}
AnimationIndex:{currentAnimationIndex}
NextUpdate: {$nextUpdate}
Progress: {progress.toFixed(2)}
lastAdjustProgress: {lastAdjustProgress}
scrollPosition: {lyricsContainer.scrollTop}
</p>
</div>
{/if}
{#if lyrics && originalLyrics && originalLyrics.scripts}
<div <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-[45vw] xl:px-[3vw] h-[calc(100vh-17rem)] xl:h-screen font-sans 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 lyrics" text-left no-scrollbar overflow-y-auto z-[1] pt-16 lyrics"
bind:this={lyricsContainer} bind:this={lyricsContainer}
on:scroll={scrollHandler}
> >
{#if debugMode}
<p class="fixed top-6 right-20 font-mono text-sm">
LyricIndex: {currentLyricIndex} PositionIndex: {currentPositionIndex} AnimationIndex:{currentAnimationIndex}
NextUpdate: {nextUpdate}
Progress: {progress.toFixed(2)}
lastAdjustProgress: {lastAdjustProgress}
</p>
{/if}
{#each lyrics as lyric, i} {#each lyrics as lyric, i}
<p bind:this={_refs[i]} class={`${getClass(i, progress)} text-shadow-lg`}> <p bind:this={_refs[i]} class={`${getClass(i, progress)} text-shadow-lg`}>
{#if debugMode} {#if debugMode && refs[i] && refs[i].style !== undefined}
<span class="text-lg absolute">{i}</span> <span class="text-lg absolute -translate-y-4">{i} &nbsp;
{originalLyrics.scripts[i].start} ~ {originalLyrics.scripts[i].end}
tY: {extractTranslateValue(refs[i].style.transform)}
top: {Math.round(refs[i].getBoundingClientRect().top)}px
</span>
{/if} {/if}
{lyric} {lyric}
</p> </p>
@ -268,82 +307,103 @@
</div> </div>
{/if} {/if}
<!--suppress CssUnusedSymbol -->
<style> <style>
:root {
--lyric-mobile-font-size: 2rem;
--lyric-mobile-line-height: 2.4rem;
--lyric-mobile-margin: 1.5rem 0;
--lyric-mobile-font-weight: 700;
--lyric-desktop-font-size: 3.5rem;
--lyric-desktop-line-height: 4.5rem;
--lyric-desktop-margin: 1.75rem 0;
}
.lyrics { .lyrics {
mask-image: linear-gradient( mask-image: linear-gradient(
rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 2rem, rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 5rem), rgba(0, 0, 0, 1) calc(100% - 5rem),
rgba(0, 0, 0, 0) 100% rgba(0, 0, 0, 0) 100%
); );
} }
.no-scrollbar { .no-scrollbar {
scrollbar-width: none; scrollbar-width: none;
} }
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
width: 0px; width: 0;
} }
.current-lyric { .current-lyric {
position: relative; position: relative;
color: white; color: white;
font-weight: 600; font-weight: var(--lyric-mobile-font-weight);
font-size: 2.1rem; font-size: var(--lyric-mobile-font-size);
line-height: 2.7rem; line-height: var(--lyric-mobile-line-height);
margin: 1rem 0rem; margin: var(--lyric-mobile-margin);
scale: 1.02 1; scale: 1.02 1;
top: 1rem; top: 1rem;
} }
.previous-lyric { .previous-lyric {
position: relative; position: relative;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.48);
font-weight: 600; font-weight: var(--lyric-mobile-font-weight);
font-size: 2.1rem; font-size: var(--lyric-mobile-font-size);
line-height: 2.7rem; line-height: var(--lyric-mobile-line-height);
margin: 1rem 0rem; margin: var(--lyric-mobile-margin);
top: 1rem; top: 1rem;
} }
.after-lyric { .after-lyric {
position: relative; position: relative;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.48);
font-weight: 600; font-weight: var(--lyric-mobile-font-weight);
font-size: 2.1rem; font-size: var(--lyric-mobile-font-size);
line-height: 2.7rem; line-height: var(--lyric-mobile-line-height);
margin: 1rem 0rem; margin: var(--lyric-mobile-margin);
top: 1rem; top: 1rem;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.current-lyric { .current-lyric {
font-size: 3rem; font-size: 3rem;
line-height: 4rem; line-height: 4rem;
margin: 2.4rem 0rem; margin: 2.4rem 0;
} }
.after-lyric { .after-lyric {
font-size: 3rem; font-size: 3rem;
line-height: 3.3rem; line-height: 3.3rem;
margin: 2.4rem 0rem; margin: 2.4rem 0;
} }
.previous-lyric { .previous-lyric {
font-size: 3em; font-size: 3em;
line-height: 3.3rem; line-height: 3.3rem;
margin: 2.4rem 0rem; margin: 2.4rem 0;
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.current-lyric { .current-lyric {
font-size: 3.5rem; font-size: var(--lyric-desktop-font-size);
line-height: 6.5rem; line-height: var(--lyric-desktop-line-height);
margin: 0rem 0rem; margin: var(--lyric-desktop-margin);
} }
.after-lyric { .after-lyric {
font-size: 3.5rem; font-size: var(--lyric-desktop-font-size);
line-height: 6.5rem; line-height: var(--lyric-desktop-line-height);
margin: 0rem 0rem; margin: var(--lyric-desktop-margin);
} }
.previous-lyric { .previous-lyric {
font-size: 3.5rem; font-size: var(--lyric-desktop-font-size);
line-height: 6.5rem; line-height: var(--lyric-desktop-line-height);
margin: 0rem 0rem; margin: var(--lyric-desktop-margin);
} }
} }
</style> </style>

View File

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
const nextUpdate = writable(-1);
export default nextUpdate;

View File

@ -1,27 +1,28 @@
import { songNameCache } from '$lib/server/cache.js'; import { songNameCache } from '$lib/server/cache.js';
import { loadData } from '$lib/server/database/loadData'; import { loadData } from '$lib/server/database/loadData';
import { json, error } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export async function GET({ url }) { export const GET: RequestHandler = async ({ url }) => {
const keyword = url.searchParams.get("keyword"); const keyword = url.searchParams.get('keyword');
loadData(); await loadData();
if (keyword === null) { if (keyword === null) {
return error(400, { return error(400, {
"message": "Miss parameter: keyword" 'message': 'Miss parameter: keyword'
}) });
} }
const resultList: MusicMetadata[] = []; const resultList: MusicMetadata[] = [];
for (const songName of songNameCache.keys()){ for (const songName of songNameCache.keys()) {
if (songName.toLocaleLowerCase().includes(keyword.toLocaleLowerCase())) { if (songName.toLocaleLowerCase().includes(keyword.toLocaleLowerCase())) {
resultList.push(songNameCache.get(songName)!); resultList.push(songNameCache.get(songName)!);
} }
} }
return json({ return json({
"result": resultList 'result': resultList
}); });
} };

View File

@ -1,19 +1,20 @@
import { getCurrentFormattedDateTime } from '$lib/songUpdateTime'; import { getCurrentFormattedDateTime } from '$lib/songUpdateTime';
import { json, error } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import fs from 'fs'; import fs from 'fs';
import type { RequestHandler } from './$types';
export async function GET({ params }) { export const GET: RequestHandler = async ({ params }) => {
const filePath = `./data/song/${params.id}.json`; const filePath = `./data/song/${params.id}.json`;
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
return error(404, { return error(404, {
message: "No correspoding song." message: "No corresponding song."
}) })
} }
const data = fs.readFileSync(filePath); const data = fs.readFileSync(filePath);
return json(JSON.parse(data.toString())); return json(JSON.parse(data.toString()));
} }
export async function POST({ params, request }) { export const POST: RequestHandler = async ({ request, params }) => {
const timeStamp = new Date().getTime(); const timeStamp = new Date().getTime();
if (!fs.existsSync("./data/pending/")) { if (!fs.existsSync("./data/pending/")) {
fs.mkdirSync("./data/pending"); fs.mkdirSync("./data/pending");

View File

@ -1,8 +1,9 @@
import { songData } from '$lib/server/cache.js'; import { songData } from '$lib/server/cache.js';
import { loadData } from '$lib/server/database/loadData.js'; import { loadData } from '$lib/server/database/loadData.js';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export async function GET({ url }) { export const GET: RequestHandler = async ({ url }) => {
const limit = parseInt(url.searchParams.get("limit") ?? "20"); const limit = parseInt(url.searchParams.get("limit") ?? "20");
const offset = parseInt(url.searchParams.get("offset") ?? "0"); const offset = parseInt(url.searchParams.get("offset") ?? "0");
loadData(); loadData();

View File

@ -1,8 +1,8 @@
/** @type {import('./$types').PageLoad} */ import type { PageServerLoad } from './$types';
import fs from 'fs'; import fs from 'fs';
export function load({ params }) { export const load: PageServerLoad = ({ params }) => {
const filePath = `./data/song/${params.id}.json`; const filePath = `./data/song/${params.id}.json`;
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
return { return {