merge: update from dev branch
This commit is contained in:
parent
74d783b5d5
commit
ab8c32ffb8
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -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
5
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
12
.idea/AquaVox.iml
Normal file
12
.idea/AquaVox.iml
Normal 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>
|
48
.idea/codeStyles/Project.xml
Normal file
48
.idea/codeStyles/Project.xml
Normal 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>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
6
.idea/jsLibraryMappings.xml
Normal file
6
.idea/jsLibraryMappings.xml
Normal 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
8
.idea/modules.xml
Normal 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>
|
17
.idea/runConfigurations/Run_Dev_Server.xml
Normal file
17
.idea/runConfigurations/Run_Dev_Server.xml
Normal 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
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aquavox",
|
||||
"version": "1.12.8",
|
||||
"version": "1.12.12",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
@ -1,3 +0,0 @@
|
||||
<div>
|
||||
|
||||
</div>
|
@ -35,11 +35,11 @@
|
||||
}
|
||||
|
||||
function volumeBarChangeTouch(e: TouchEvent) {
|
||||
const value = turncate(
|
||||
e.touches[0].clientX - volumeBar.getBoundingClientRect().x,
|
||||
0,
|
||||
volumeBar.getBoundingClientRect().width
|
||||
) / volumeBar.getBoundingClientRect().width;
|
||||
const value = truncate(
|
||||
e.touches[0].clientX - volumeBar.getBoundingClientRect().x,
|
||||
0,
|
||||
volumeBar.getBoundingClientRect().width
|
||||
) / volumeBar.getBoundingClientRect().width;
|
||||
adjustVolume(value);
|
||||
localStorage.setItem('volume', value.toString());
|
||||
}
|
||||
@ -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={() => {
|
||||
userAdjustingProgress.set(false);
|
||||
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) {
|
||||
e.preventDefault();
|
||||
}
|
||||
userAdjustingVolume = true;
|
||||
on:mouseup={() => {
|
||||
userAdjustingVolume = false;
|
||||
}}
|
||||
on:touchend={(e) => {
|
||||
e.preventDefault();
|
||||
userAdjustingVolume = false;
|
||||
}}
|
||||
on:touchmove={(e) => {
|
||||
e.preventDefault();
|
||||
@ -193,24 +204,18 @@
|
||||
volumeBarChangeTouch(e);
|
||||
}
|
||||
}}
|
||||
on:touchend={(e) => {
|
||||
e.preventDefault();
|
||||
userAdjustingVolume = false;
|
||||
}}
|
||||
on:mouseup={() => {
|
||||
userAdjustingVolume = false;
|
||||
on:touchstart={(e) => {
|
||||
if (e.cancelable) {
|
||||
e.preventDefault();
|
||||
}
|
||||
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,19 +262,21 @@
|
||||
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%,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 5rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 5rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
@ -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;
|
||||
lyricsContainer.scrollTop += h;
|
||||
setTimeout(() => {
|
||||
scriptScrolling = false;
|
||||
}, 500);
|
||||
if (lyricsContainer !== null) {
|
||||
lyricsContainer.scrollTop += h;
|
||||
}
|
||||
await sleep(500);
|
||||
scriptScrolling = false;
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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) {
|
||||
nextUpdate = progress;
|
||||
return;
|
||||
}
|
||||
if (!lyricsContainer || !originalLyrics.scripts) return;
|
||||
|
||||
if (nextUpdate - progress >= 0.05) return;
|
||||
if (
|
||||
currentPositionIndex < 0 ||
|
||||
currentPositionIndex === currentAnimationIndex ||
|
||||
currentPositionIndex === lastAdjustProgress ||
|
||||
scrolling
|
||||
)
|
||||
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;
|
||||
|
||||
if (originalLyrics.scripts && currentLyric.getBoundingClientRect().top < 0) return;
|
||||
|
||||
const offsetHeight =
|
||||
refs[currentPositionIndex].getBoundingClientRect().height +
|
||||
refs[currentPositionIndex].getBoundingClientRect().top -
|
||||
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)`;
|
||||
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)`;
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
{#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}
|
||||
{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,82 +307,103 @@
|
||||
</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%,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 5rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 5rem),
|
||||
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>
|
||||
|
3
src/lib/state/nextUpdate.ts
Normal file
3
src/lib/state/nextUpdate.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
const nextUpdate = writable(-1);
|
||||
export default nextUpdate;
|
@ -1,27 +1,28 @@
|
||||
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[] = [];
|
||||
|
||||
for (const songName of songNameCache.keys()){
|
||||
for (const songName of songNameCache.keys()) {
|
||||
if (songName.toLocaleLowerCase().includes(keyword.toLocaleLowerCase())) {
|
||||
resultList.push(songNameCache.get(songName)!);
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
"result": resultList
|
||||
return json({
|
||||
'result': resultList
|
||||
});
|
||||
}
|
||||
};
|
@ -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");
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user