feature: dynamic lyrics
This commit is contained in:
parent
8d6089511d
commit
4422937707
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -198,7 +198,8 @@
|
||||
.interactive-box {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
width: 34vw;
|
||||
width: 55vh;
|
||||
min-width: 27vw;
|
||||
top: 69vh;
|
||||
height: 15rem;
|
||||
left: 10vw;
|
||||
|
@ -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;
|
||||
|
@ -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} />
|
||||
|
@ -19,4 +19,4 @@ export default defineConfig({
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user