feature: dynamic lyrics

This commit is contained in:
Alikia2x 2024-05-12 05:45:37 +08:00
parent 8d6089511d
commit 4422937707
8 changed files with 138 additions and 56 deletions

View File

@ -35,10 +35,12 @@
},
"type": "module",
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"jotai": "^2.8.0",
"jotai-svelte": "^0.0.2",
"localforage": "^1.10.0",
"music-metadata-browser": "^2.5.10",
"srt-parser-2": "^1.2.3",
"uuid": "^9.0.1"
}
}

View File

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@esbuild-plugins/node-globals-polyfill':
specifier: ^0.2.3
version: 0.2.3(esbuild@0.20.2)
jotai:
specifier: ^2.8.0
version: 2.8.0
@ -17,6 +20,9 @@ dependencies:
music-metadata-browser:
specifier: ^2.5.10
version: 2.5.10
srt-parser-2:
specifier: ^1.2.3
version: 1.2.3
uuid:
specifier: ^9.0.1
version: 9.0.1
@ -95,13 +101,20 @@ packages:
'@jridgewell/trace-mapping': 0.3.25
dev: true
/@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.20.2):
resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
peerDependencies:
esbuild: '*'
dependencies:
esbuild: 0.20.2
dev: false
/@esbuild/aix-ppc64@0.20.2:
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-arm64@0.20.2:
@ -110,7 +123,6 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-arm@0.20.2:
@ -119,7 +131,6 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-x64@0.20.2:
@ -128,7 +139,6 @@ packages:
cpu: [x64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-arm64@0.20.2:
@ -137,7 +147,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-x64@0.20.2:
@ -146,7 +155,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-arm64@0.20.2:
@ -155,7 +163,6 @@ packages:
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-x64@0.20.2:
@ -164,7 +171,6 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm64@0.20.2:
@ -173,7 +179,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm@0.20.2:
@ -182,7 +187,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ia32@0.20.2:
@ -191,7 +195,6 @@ packages:
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-loong64@0.20.2:
@ -200,7 +203,6 @@ packages:
cpu: [loong64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-mips64el@0.20.2:
@ -209,7 +211,6 @@ packages:
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ppc64@0.20.2:
@ -218,7 +219,6 @@ packages:
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-riscv64@0.20.2:
@ -227,7 +227,6 @@ packages:
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-s390x@0.20.2:
@ -236,7 +235,6 @@ packages:
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-x64@0.20.2:
@ -245,7 +243,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/netbsd-x64@0.20.2:
@ -254,7 +251,6 @@ packages:
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/openbsd-x64@0.20.2:
@ -263,7 +259,6 @@ packages:
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/sunos-x64@0.20.2:
@ -272,7 +267,6 @@ packages:
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-arm64@0.20.2:
@ -281,7 +275,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-ia32@0.20.2:
@ -290,7 +283,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-x64@0.20.2:
@ -299,7 +291,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
@ -1138,7 +1129,6 @@ packages:
'@esbuild/win32-arm64': 0.20.2
'@esbuild/win32-ia32': 0.20.2
'@esbuild/win32-x64': 0.20.2
dev: true
/escalade@3.1.2:
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
@ -2393,6 +2383,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/srt-parser-2@1.2.3:
resolution: {integrity: sha512-dANP1AyJTI503H0/kXwRza+7QxDB3BqeFvEKTF4MI9lQcBe8JbRUQTKVIGzGABJCwBovEYavZ2Qsdm/s8XKz8A==}
hasBin: true
dev: false
/stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
dev: true

View File

@ -2,7 +2,7 @@
import localforage from '$lib/storage';
import type { Writable } from 'svelte/store';
export let coverPath: Writable<string>;
let path: string = "";
let path: string = '';
coverPath.subscribe((p) => {
if (p) path = p;
@ -16,9 +16,9 @@
user-select: none;
position: absolute;
z-index: 1;
min-height: 35vh;
max-height: 55vh;
width: 34vw;
width: 55vh;
min-width: 27vw;
min-height: 27vw;
object-fit: cover;
left: 10vw;
top: 40vh;

View File

@ -10,19 +10,23 @@
import formatDuration from '$lib/formatDuration';
const items = useAtom(fileListState);
const finalItems = useAtom(finalFileListState);
let displayItems: any[] = [];
$: {
const length = $items.length;
for (let i = 0; i < length; i++) {
if ($items[i].type.indexOf('audio') === -1) continue;
if ($items[i].type.indexOf('audio') === -1) {
finalItems.update((prev) => {
return [...prev, $items[i]];
});
continue;
}
if ($items[i].pic || $items[i].pic === 'N/A') continue;
getAudioMeta($items[i], (metadata: IAudioMetadata) => {
let cover: string | null = null;
let duration: number | null = null;
if (metadata.common.picture)
cover = convertCoverData(metadata.common.picture[0]);
if (metadata.format.duration)
duration = metadata.format.duration;
if (metadata.common.picture) cover = convertCoverData(metadata.common.picture[0]);
if (metadata.format.duration) duration = metadata.format.duration;
finalItems.update((prev) => {
if (cover) {
let currentItem = [];
@ -41,27 +45,38 @@
});
}
}
$: {
// remove duplicated
displayItems = $finalItems.filter((item, index) => {
return $finalItems.indexOf(item) === index;
})
}
</script>
<ul
class="mt-4 relative w-full min-h-48 max-h-[27rem] overflow-y-auto bg-zinc-200 dark:bg-zinc-800 rounded"
>
{#each $items as item}
{#each displayItems as item}
<li class="relative m-4 p-4 bg-zinc-300 dark:bg-zinc-600 rounded-lg">
<span>{extractFileName(item.name)}</span> <br />
<span>{toHumanSize(item.size)}</span>
{#if item.type}
· <span>{formatText(item.type)}</span>
{:else if item.name.split(".").length > 1}
· <span>{formatText(item.name.split(".")[item.name.split(".").length-1])}</span>
· <span>{formatText(item.type)}</span>
{:else if item.name.split('.').length > 1}
· <span>{formatText(item.name.split('.')[item.name.split('.').length - 1])}</span>
{:else}
· <span>未知格式</span>
· <span>未知格式</span>
{/if}
{#if item.duration}
· <span>{formatDuration(item.duration)}</span>
· <span>{formatDuration(item.duration)}</span>
{/if}
{#if item.pic!==undefined && item.pic !== 'N/A'}
<img class="h-16 w-16 object-cover absolute rounded-lg right-2 top-2" src={item.pic} alt="" />
{#if item.pic !== undefined && item.pic !== 'N/A'}
<img
class="h-16 w-16 object-cover absolute rounded-lg right-2 top-2"
src={item.pic}
alt=""
/>
{/if}
</li>
{/each}

View File

@ -198,7 +198,8 @@
.interactive-box {
user-select: none;
position: absolute;
width: 34vw;
width: 55vh;
min-width: 27vw;
top: 69vh;
height: 15rem;
left: 10vw;

View File

@ -1,19 +1,81 @@
<script lang="ts">
export let lyrics: string[] = [];
import type { Line } from 'srt-parser-2';
export let lyrics: string[];
export let originalLyrics: Line[];
export let progress: number;
let currentScrollPos = '';
let currentLyric: Line;
let currentLyricIndex = -1;
let refs = [];
let _refs: any[] = [];
$: refs = _refs.filter(Boolean);
function getClass(lyric: string, progress: number) {
if (lyric === currentLyric.text) return 'current-lyric';
else if (progress > currentLyric.endSeconds) return 'after-lyric';
else return 'previous-lyric';
}
$: {
if (originalLyrics) {
let found = false;
for (let i = 0; i < originalLyrics.length; i++) {
let l = originalLyrics[i];
if (progress >= l.startSeconds && progress <= l.endSeconds) {
currentLyric = l;
currentLyricIndex = i;
found = true;
const currentRef = refs[i];
if (currentRef && currentScrollPos !== currentLyric.text) {
currentRef.scrollIntoView({ behavior: 'smooth', block: 'center' });
currentScrollPos = currentLyric.text;
}
break;
}
}
for (let i = 0; i < lyrics.length; i++) {
const offset = Math.abs(i - currentLyricIndex);
const blurRadius = Math.min(offset * 1, 16);
const fontSize = i === currentLyricIndex ? '3.5rem' : '3rem';
const lineHeight = i === currentLyricIndex ? '4.5rem' : '4rem';
if (refs[i]) {
refs[i].style.filter = `blur(${blurRadius}px)`;
refs[i].style.fontSize = fontSize;
refs[i].style.lineHeight = lineHeight;
}
}
if (!found) {
currentLyric = {
id: '-1',
startTime: '00:00:00,000',
startSeconds: 0,
endTime: '00:00:00,000',
endSeconds: 0,
text: ''
};
}
}
}
</script>
<div class="lyrics" style="overflow-y: auto">
{#each lyrics as lyric}
<p class="current-lyric">{lyric}</p>
{/each}
</div>
{#if lyrics && originalLyrics}
<div class="lyrics" style="overflow-y: auto">
{#each lyrics as lyric, i}
<p bind:this={_refs[i]} class={getClass(lyric, progress)}>
{lyric}
</p>
{/each}
</div>
{/if}
<style>
.lyrics {
position: absolute;
width: 45vw;
left: 50vw;
width: 52vw;
left: 45vw;
padding-left: 3vw;
padding-right: 3vw;
height: 100vh;
font-family: sans-serif;
text-align: left;

View File

@ -8,6 +8,8 @@
import extractFileName from '$lib/extractFileName';
import localforage from 'localforage';
import { writable } from 'svelte/store';
import srtParser2 from 'srt-parser-2';
import type { Line } from 'srt-parser-2';
const audioId = $page.params.id;
let audioPlayer: HTMLAudioElement;
@ -20,7 +22,8 @@
let paused: boolean = true;
let launched = false;
let prepared: string[] = [];
let originalLyrics: string = '';
let originalLyrics: Line[];
let lyricsText: string[] = [];
const coverPath = writable('');
let mainInterval: ReturnType<typeof setInterval>;
@ -81,11 +84,15 @@
prepared.push('file');
}
});
localforage.getItem(`${audioId}-lyrics`, function (err, file) {
localforage.getItem(`${audioId}-lyric`, function (err, file) {
if (file) {
const f = file as File;
f.text().then((lr) => {
originalLyrics = lr;
const parser = new srtParser2();
originalLyrics = parser.fromSrt(lr);
for (const line of originalLyrics) {
lyricsText.push(line.text);
}
});
}
});
@ -136,7 +143,7 @@
if (audioPlayer !== null && audioPlayer.currentTime !== undefined) {
currentProgress = audioPlayer.currentTime;
}
}, 250);
}, 100);
}
$: {
@ -172,4 +179,4 @@
}}
></audio>
<Lyrics lyrics={[]} progress={currentProgress} />
<Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} />

View File

@ -19,4 +19,4 @@ export default defineConfig({
]
}
}
});
});