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",
"version": "1.12.8",
"version": "1.12.12",
"private": false,
"scripts": {
"dev": "vite dev",

View File

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

View File

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

View File

@ -4,6 +4,7 @@
import progressBarRaw from '$lib/state/progressBarRaw';
import type { LrcJsonData } from 'lrc-parser-ts';
import progressBarSlideValue from '$lib/state/progressBarSlideValue';
import nextUpdate from '$lib/state/nextUpdate';
// Component input properties
export let lyrics: string[];
@ -12,18 +13,25 @@
// Local state and variables
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 currentPositionIndex = -1;
let currentAnimationIndex = -1;
let lyricsContainer: HTMLDivElement;
let nextUpdate = -1;
let lyricsContainer: HTMLDivElement | null;
let lastAdjustProgress = 0;
let localProgress = 0;
let lastScroll = 0;
let scrolling = false;
let scriptScrolling = false;
let currentLyricTopMargin = 288;
// References to lyric elements
let refs: HTMLParagraphElement[] = [];
let _refs: any[] = [];
@ -38,73 +46,37 @@
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
async function moveToNextLine(h: number) {
let pos = currentPositionIndex + 2;
for (let i = currentPositionIndex + 2; i < refs.length; i++) {
console.debug(new Date().getTime() , 'moveToNextLine', h);
// 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];
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)`;
pos = i;
processingLineIndex = i;
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) {
for (let i = pos; i < refs.length; i++) {
if (refs.length - processingLineIndex < 3) {
for (let i = processingLineIndex; i < refs.length; i++) {
const lyric = refs[i];
lyric.style.transition =
'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)`;
pos = i;
processingLineIndex = i;
await sleep(75);
}
} else {
for (let i = pos; i < refs.length; i++) {
for (let i = processingLineIndex; i < refs.length; i++) {
refs[i].style.transition =
'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
const height = refs[i].getBoundingClientRect().height;
@ -112,37 +84,45 @@
}
}
// wait until the animation end
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++) {
refs[i].style.transition =
'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++) {
refs[i].style.transform = `translateY(0px)`;
}
scriptScrolling = true;
if (lyricsContainer !== null) {
lyricsContainer.scrollTop += h;
setTimeout(() => {
}
await sleep(500);
scriptScrolling = false;
}, 500);
}
// Scroll the lyrics container to the given lyric
async function scrollToLyric(currentLyric: HTMLParagraphElement) {
if (!originalLyrics || !originalLyrics.scripts || !lyricsContainer) return;
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(() => {
scriptScrolling = false;
}, 500);
}
// Handle user adjusting progress state changes
userAdjustingProgress.subscribe((v) => {
userAdjustingProgress.subscribe((adjusting) => {
if (!originalLyrics) return;
const scripts = originalLyrics.scripts;
if (!scripts) return;
if (v) {
if (adjusting) {
for (let i = 0; i < scripts.length; i++) {
refs[i].style.filter = `blur(0px)`;
}
@ -193,73 +173,132 @@
}, 5500);
}
// Update lyrics position based on progress
$: {
(() => {
if ($userAdjustingProgress) {
nextUpdate = progress;
return;
// Utility function to create a sleep/delay
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
if (nextUpdate - progress >= 0.05) return;
// 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.set(cl.end);
} else {
currentLyricIndex = -1;
nextUpdate.set(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 * 0.96, 16);
if (refs[i]) {
refs[i].style.filter = `blur(${blurRadius}px)`;
}
}
})();
}
nextUpdate.subscribe(async (nextUpdate) => {
if (
currentPositionIndex < 0 ||
currentPositionIndex === currentAnimationIndex ||
currentPositionIndex === lastAdjustProgress ||
$userAdjustingProgress === true ||
scrolling
)
return;
) return;
const currentLyric = refs[currentPositionIndex];
if (originalLyrics.scripts && currentLyric.getBoundingClientRect().top < 0) return;
const offsetHeight =
refs[currentPositionIndex].getBoundingClientRect().height +
refs[currentPositionIndex].getBoundingClientRect().top -
144;
currentLyricTopMargin;
currentLyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
// 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';
const height = refs[i].getBoundingClientRect().height;
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.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);
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>
{#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
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"
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}
<p bind:this={_refs[i]} class={`${getClass(i, progress)} text-shadow-lg`}>
{#if debugMode}
<span class="text-lg absolute">{i}</span>
{#if debugMode && refs[i] && refs[i].style !== undefined}
<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}
{lyric}
</p>
@ -268,7 +307,18 @@
</div>
{/if}
<!--suppress CssUnusedSymbol -->
<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 {
mask-image: linear-gradient(
rgba(0, 0, 0, 0) 0%,
@ -277,73 +327,83 @@
rgba(0, 0, 0, 0) 100%
);
}
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
width: 0px;
width: 0;
}
.current-lyric {
position: relative;
color: white;
font-weight: 600;
font-size: 2.1rem;
line-height: 2.7rem;
margin: 1rem 0rem;
font-weight: var(--lyric-mobile-font-weight);
font-size: var(--lyric-mobile-font-size);
line-height: var(--lyric-mobile-line-height);
margin: var(--lyric-mobile-margin);
scale: 1.02 1;
top: 1rem;
}
.previous-lyric {
position: relative;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
font-size: 2.1rem;
line-height: 2.7rem;
margin: 1rem 0rem;
color: rgba(255, 255, 255, 0.48);
font-weight: var(--lyric-mobile-font-weight);
font-size: var(--lyric-mobile-font-size);
line-height: var(--lyric-mobile-line-height);
margin: var(--lyric-mobile-margin);
top: 1rem;
}
.after-lyric {
position: relative;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
font-size: 2.1rem;
line-height: 2.7rem;
margin: 1rem 0rem;
color: rgba(255, 255, 255, 0.48);
font-weight: var(--lyric-mobile-font-weight);
font-size: var(--lyric-mobile-font-size);
line-height: var(--lyric-mobile-line-height);
margin: var(--lyric-mobile-margin);
top: 1rem;
}
@media (min-width: 768px) {
.current-lyric {
font-size: 3rem;
line-height: 4rem;
margin: 2.4rem 0rem;
margin: 2.4rem 0;
}
.after-lyric {
font-size: 3rem;
line-height: 3.3rem;
margin: 2.4rem 0rem;
margin: 2.4rem 0;
}
.previous-lyric {
font-size: 3em;
line-height: 3.3rem;
margin: 2.4rem 0rem;
margin: 2.4rem 0;
}
}
@media (min-width: 1024px) {
.current-lyric {
font-size: 3.5rem;
line-height: 6.5rem;
margin: 0rem 0rem;
font-size: var(--lyric-desktop-font-size);
line-height: var(--lyric-desktop-line-height);
margin: var(--lyric-desktop-margin);
}
.after-lyric {
font-size: 3.5rem;
line-height: 6.5rem;
margin: 0rem 0rem;
font-size: var(--lyric-desktop-font-size);
line-height: var(--lyric-desktop-line-height);
margin: var(--lyric-desktop-margin);
}
.previous-lyric {
font-size: 3.5rem;
line-height: 6.5rem;
margin: 0rem 0rem;
font-size: var(--lyric-desktop-font-size);
line-height: var(--lyric-desktop-line-height);
margin: var(--lyric-desktop-margin);
}
}
</style>

View File

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

View File

@ -1,16 +1,17 @@
import { songNameCache } from '$lib/server/cache.js';
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 }) {
const keyword = url.searchParams.get("keyword");
export const GET: RequestHandler = async ({ url }) => {
const keyword = url.searchParams.get('keyword');
loadData();
await loadData();
if (keyword === null) {
return error(400, {
"message": "Miss parameter: keyword"
})
'message': 'Miss parameter: keyword'
});
}
const resultList: MusicMetadata[] = [];
@ -22,6 +23,6 @@ export async function GET({ url }) {
}
return json({
"result": resultList
'result': resultList
});
}
};

View File

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

View File

@ -1,8 +1,9 @@
import { songData } from '$lib/server/cache.js';
import { loadData } from '$lib/server/database/loadData.js';
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 offset = parseInt(url.searchParams.get("offset") ?? "0");
loadData();

View File

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