261 lines
8.4 KiB
Svelte
261 lines
8.4 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/stores';
|
|
import getAudioIDMetadata from '$lib/audio/getAudioIDMetadata';
|
|
import Background from '$lib/components/background.svelte';
|
|
import Cover from '$lib/components/cover.svelte';
|
|
import InteractiveBox from '$lib/components/interactiveBox.svelte';
|
|
import extractFileName from '$lib/utils/extractFileName';
|
|
import localforage from 'localforage';
|
|
import { writable } from 'svelte/store';
|
|
import userAdjustingProgress from '$lib/state/userAdjustingProgress';
|
|
import type { IAudioMetadata } from 'music-metadata-browser';
|
|
import { onDestroy, onMount } from 'svelte';
|
|
import progressBarRaw from '$lib/state/progressBarRaw';
|
|
import { parseTTML, type TTMLLyric } from '$lib/ttml';
|
|
import type { LyricLine, LyricLineMouseEvent, LyricPlayer } from '@applemusic-like-lyrics/core';
|
|
import NewLyrics from '$lib/components/newLyrics.svelte';
|
|
import { LyricPlayer as CoreLyricPlayer } from '@applemusic-like-lyrics/core';
|
|
import { parseLrc } from '@applemusic-like-lyrics/lyric';
|
|
import { mapLyric } from '$lib/lyrics/mapLyric';
|
|
|
|
const audioId = $page.params.id;
|
|
let audioPlayer: HTMLAudioElement | null = null;
|
|
let volume = 1;
|
|
let name = '';
|
|
let singer = '';
|
|
let duration = 0;
|
|
let currentProgress = 0;
|
|
let audioFile: File;
|
|
let paused: boolean = true;
|
|
let launched = false;
|
|
let prepared: string[] = [];
|
|
let lyricLines: LyricLine[];
|
|
let hasLyrics: boolean;
|
|
let lyricPlayer: LyricPlayer = new CoreLyricPlayer();
|
|
const coverPath = writable('');
|
|
let mainInterval: ReturnType<typeof setInterval>;
|
|
|
|
function setMediaSession() {
|
|
if ('mediaSession' in navigator === false) return;
|
|
const ms = navigator.mediaSession;
|
|
ms.metadata = new MediaMetadata({
|
|
title: name,
|
|
artist: singer,
|
|
artwork: [
|
|
{
|
|
src: $coverPath
|
|
}
|
|
]
|
|
});
|
|
ms.setActionHandler('play', function () {
|
|
if (audioPlayer === null) return;
|
|
audioPlayer.play();
|
|
paused = false;
|
|
});
|
|
|
|
ms.setActionHandler('pause', function () {
|
|
if (audioPlayer === null) return;
|
|
audioPlayer.pause();
|
|
paused = true;
|
|
});
|
|
|
|
ms.setActionHandler('seekbackward', function () {
|
|
if (audioPlayer === null) return;
|
|
if (audioPlayer.currentTime > 4) {
|
|
audioPlayer.currentTime = 0;
|
|
}
|
|
});
|
|
|
|
ms.setActionHandler('previoustrack', function () {
|
|
if (audioPlayer === null) return;
|
|
if (audioPlayer.currentTime > 4) {
|
|
audioPlayer.currentTime = 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
function readDB() {
|
|
getAudioIDMetadata(audioId, (metadata: IAudioMetadata | null) => {
|
|
if (!metadata) return;
|
|
duration = metadata.format.duration ? metadata.format.duration : 0;
|
|
singer = metadata.common.artist ? metadata.common.artist : '未知歌手';
|
|
prepared.push('duration');
|
|
});
|
|
localforage.getItem(`${audioId}-cover`, function (err, file) {
|
|
if (file) {
|
|
const path = URL.createObjectURL(file as File);
|
|
coverPath.set(path);
|
|
}
|
|
prepared.push('cover');
|
|
});
|
|
localforage.getItem(`${audioId}-file`, function (err, file) {
|
|
if (audioPlayer === null) return;
|
|
if (file) {
|
|
const f = file as File;
|
|
audioFile = f;
|
|
audioPlayer.src = URL.createObjectURL(audioFile);
|
|
name = extractFileName(f.name);
|
|
prepared.push('name');
|
|
prepared.push('file');
|
|
}
|
|
});
|
|
localforage.getItem(`${audioId}-lyric`, function (err, file) {
|
|
if (file) {
|
|
const f = file as File;
|
|
f.text().then((lr) => {
|
|
if (f.name.endsWith('.ttml')) {
|
|
lyricLines = parseTTML(lr).lyricLines;
|
|
hasLyrics = true;
|
|
} else if (f.name.endsWith('.lrc')) {
|
|
lyricLines = parseLrc(lr).map((line, i, lines) => ({
|
|
words: [
|
|
{
|
|
word: line.words[0]?.word ?? '',
|
|
startTime: line.words[0]?.startTime ?? 0,
|
|
endTime: lines[i + 1]?.words?.[0]?.startTime ?? Infinity
|
|
}
|
|
],
|
|
startTime: line.words[0]?.startTime ?? 0,
|
|
endTime: lines[i + 1]?.words?.[0]?.startTime ?? Infinity,
|
|
translatedLyric: '',
|
|
romanLyric: '',
|
|
isBG: false,
|
|
isDuet: false
|
|
}));
|
|
hasLyrics = true;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function playAudio() {
|
|
if (audioPlayer === null) return;
|
|
if (audioPlayer.duration) {
|
|
duration = audioPlayer.duration;
|
|
}
|
|
audioPlayer.paused ? audioPlayer.play() : audioPlayer.pause();
|
|
paused = audioPlayer.paused;
|
|
setMediaSession();
|
|
}
|
|
|
|
$: {
|
|
if (!launched && audioPlayer) {
|
|
const requirements = ['name', 'file', 'cover'];
|
|
let flag = true;
|
|
for (const r of requirements) {
|
|
if (!prepared.includes(r)) {
|
|
flag = false;
|
|
}
|
|
}
|
|
if (flag) {
|
|
launched = true;
|
|
setMediaSession();
|
|
audioPlayer.play();
|
|
}
|
|
}
|
|
}
|
|
|
|
function adjustProgress(progress: number) {
|
|
if (audioPlayer) {
|
|
audioPlayer.currentTime = duration * progress;
|
|
currentProgress = duration * progress;
|
|
lyricPlayer.calcLayout(false, true);
|
|
}
|
|
}
|
|
|
|
function adjustDisplayProgress(progress: number) {
|
|
if (audioPlayer) {
|
|
currentProgress = duration * progress;
|
|
}
|
|
}
|
|
|
|
function adjustVolume(targetVolume: number) {
|
|
if (audioPlayer) {
|
|
audioPlayer.volume = targetVolume;
|
|
}
|
|
}
|
|
|
|
$: {
|
|
clearInterval(mainInterval);
|
|
mainInterval = setInterval(() => {
|
|
if (audioPlayer === null) return;
|
|
if ($userAdjustingProgress === false) currentProgress = audioPlayer.currentTime;
|
|
progressBarRaw.set(audioPlayer.currentTime);
|
|
}, 50);
|
|
}
|
|
|
|
onMount(() => {
|
|
if (audioPlayer === null) return;
|
|
audioPlayer.volume = localStorage.getItem('volume') ? Number(localStorage.getItem('volume')) : 1;
|
|
});
|
|
onDestroy(() => {
|
|
if (audioPlayer === null) return;
|
|
});
|
|
|
|
$: {
|
|
if (audioPlayer) {
|
|
paused = audioPlayer.paused;
|
|
volume = audioPlayer.volume;
|
|
}
|
|
}
|
|
|
|
function onLyricLineClick(e: LyricLineMouseEvent) {
|
|
lyricPlayer.resetScroll();
|
|
adjustProgress(lyricLines[e.lineIndex].startTime / 1000 / duration);
|
|
}
|
|
|
|
readDB();
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{name} - AquaVox</title>
|
|
</svelte:head>
|
|
|
|
<Background coverId={audioId} />
|
|
<Cover {coverPath} {hasLyrics} />
|
|
<InteractiveBox
|
|
{name}
|
|
{singer}
|
|
{duration}
|
|
{volume}
|
|
progress={currentProgress}
|
|
clickPlay={playAudio}
|
|
{paused}
|
|
{adjustProgress}
|
|
{adjustVolume}
|
|
{adjustDisplayProgress}
|
|
{hasLyrics}
|
|
/>
|
|
|
|
<NewLyrics
|
|
{lyricPlayer}
|
|
{lyricLines}
|
|
currentTime={Math.round(currentProgress * 1000)}
|
|
playing={!paused}
|
|
{onLyricLineClick}
|
|
alignPosition={0.3}
|
|
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] font-semibold mix-blend-plus-lighter"
|
|
/>
|
|
|
|
<audio
|
|
bind:this={audioPlayer}
|
|
controls
|
|
style="display: none"
|
|
on:play={() => {
|
|
if (audioPlayer === null) return;
|
|
paused = audioPlayer.paused;
|
|
}}
|
|
on:pause={() => {
|
|
if (audioPlayer === null) return;
|
|
paused = audioPlayer.paused;
|
|
}}
|
|
on:ended={() => {
|
|
paused = true;
|
|
if (audioPlayer == null) return;
|
|
audioPlayer.pause();
|
|
}}
|
|
></audio>
|