diff --git a/bun.lockb b/bun.lockb
index 383e354..98d6a6f 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 773c30a..3850db9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "aquavox",
- "version": "1.15.0",
+ "version": "2.0.0",
"private": false,
"scripts": {
"dev": "vite dev",
@@ -34,15 +34,27 @@
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",
+ "vite-plugin-wasm": "^3.3.0",
"vitest": "^1.6.0"
},
"type": "module",
"dependencies": {
+ "@applemusic-like-lyrics/core": "^0.1.3",
+ "@applemusic-like-lyrics/lyric": "^0.2.2",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
+ "@pixi/app": "^7.4.2",
+ "@pixi/core": "^7.4.2",
+ "@pixi/display": "^7.4.2",
+ "@pixi/filter-blur": "^7.4.2",
+ "@pixi/filter-bulge-pinch": "^5.1.1",
+ "@pixi/filter-color-matrix": "^7.4.2",
+ "@pixi/sprite": "^7.4.2",
"@types/bun": "^1.1.6",
"bezier-easing": "^2.1.0",
"jotai": "^2.8.0",
"jotai-svelte": "^0.0.2",
+ "jss": "^10.10.0",
+ "jss-preset-default": "^10.10.0",
"localforage": "^1.10.0",
"lrc-parser-ts": "^1.0.3",
"music-metadata-browser": "^2.5.10",
diff --git a/src/lib/components/newLyrics.svelte b/src/lib/components/newLyrics.svelte
new file mode 100644
index 0000000..bded294
--- /dev/null
+++ b/src/lib/components/newLyrics.svelte
@@ -0,0 +1,132 @@
+
+
+
diff --git a/src/lib/lyrics/mapLyric.ts b/src/lib/lyrics/mapLyric.ts
new file mode 100644
index 0000000..13f96b2
--- /dev/null
+++ b/src/lib/lyrics/mapLyric.ts
@@ -0,0 +1,18 @@
+import type { LyricLine } from "@applemusic-like-lyrics/core";
+import {
+ type LyricLine as RawLyricLine,
+ parseLrc,
+ parseYrc,
+ parseLys,
+ parseQrc,
+} from "@applemusic-like-lyrics/lyric";
+
+export const mapLyric = (line: RawLyricLine, i: number, lines: RawLyricLine[]): LyricLine => ({
+ words: line.words,
+ startTime: line.words[0]?.startTime ?? 0,
+ endTime: line.words[line.words.length - 1]?.endTime ?? Infinity,
+ translatedLyric: '',
+ romanLyric: '',
+ isBG: false,
+ isDuet: false
+});
diff --git a/src/lib/ttml/index.ts b/src/lib/ttml/index.ts
new file mode 100644
index 0000000..79fec1c
--- /dev/null
+++ b/src/lib/ttml/index.ts
@@ -0,0 +1,3 @@
+export * from "./parser";
+export * from "./writer";
+export type * from "./ttml-types";
diff --git a/src/lib/ttml/parser.ts b/src/lib/ttml/parser.ts
new file mode 100644
index 0000000..3cdb8eb
--- /dev/null
+++ b/src/lib/ttml/parser.ts
@@ -0,0 +1,168 @@
+/**
+ * @fileoverview
+ * 解析 TTML 歌词文档到歌词数组的解析器
+ * 用于解析从 Apple Music 来的歌词文件,且扩展并支持翻译和音译文本。
+ * @see https://www.w3.org/TR/2018/REC-ttml1-20181108/
+ */
+
+import type {
+ LyricLine,
+ LyricWord,
+ TTMLLyric,
+ TTMLMetadata,
+} from "./ttml-types";
+
+const timeRegexp =
+ /^(((?[0-9]+):)?(?[0-9]+):)?(?[0-9]+([.:]([0-9]+))?)/;
+function parseTimespan(timeSpan: string): number {
+ const matches = timeRegexp.exec(timeSpan);
+ if (matches) {
+ const hour = Number(matches.groups?.hour || "0");
+ const min = Number(matches.groups?.min || "0");
+ const sec = Number(matches.groups?.sec.replace(/:/, ".") || "0");
+ return Math.floor((hour * 3600 + min * 60 + sec) * 1000);
+ }
+ throw new TypeError(`时间戳字符串解析失败:${timeSpan}`);
+}
+
+export function parseTTML(ttmlText: string): TTMLLyric {
+ const domParser = new DOMParser();
+ const ttmlDoc: XMLDocument = domParser.parseFromString(
+ ttmlText,
+ "application/xml",
+ );
+
+ let mainAgentId = "v1";
+
+ const metadata: TTMLMetadata[] = [];
+ for (const meta of ttmlDoc.querySelectorAll("meta")) {
+ if (meta.tagName === "amll:meta") {
+ const key = meta.getAttribute("key");
+ if (key) {
+ const value = meta.getAttribute("value");
+ if (value) {
+ const existing = metadata.find((m) => m.key === key);
+ if (existing) {
+ existing.value.push(value);
+ } else {
+ metadata.push({
+ key,
+ value: [value],
+ });
+ }
+ }
+ }
+ }
+ }
+
+ for (const agent of ttmlDoc.querySelectorAll("ttm\\:agent")) {
+ if (agent.getAttribute("type") === "person") {
+ const id = agent.getAttribute("xml:id");
+ if (id) {
+ mainAgentId = id;
+ }
+ }
+ }
+
+ const lyricLines: LyricLine[] = [];
+
+ function parseParseLine(lineEl: Element, isBG = false, isDuet = false) {
+ const line: LyricLine = {
+ words: [],
+ translatedLyric: "",
+ romanLyric: "",
+ isBG,
+ isDuet:
+ !!lineEl.getAttribute("ttm:agent") &&
+ lineEl.getAttribute("ttm:agent") !== mainAgentId,
+ startTime: 0,
+ endTime: 0,
+ };
+ if (isBG) line.isDuet = isDuet;
+ let haveBg = false;
+
+ for (const wordNode of lineEl.childNodes) {
+ if (wordNode.nodeType === Node.TEXT_NODE) {
+ line.words?.push({
+ word: wordNode.textContent ?? "",
+ startTime: 0,
+ endTime: 0,
+ });
+ } else if (wordNode.nodeType === Node.ELEMENT_NODE) {
+ const wordEl = wordNode as Element;
+ const role = wordEl.getAttribute("ttm:role");
+
+ if (wordEl.nodeName === "span" && role) {
+ if (role === "x-bg") {
+ parseParseLine(wordEl, true, line.isDuet);
+ haveBg = true;
+ } else if (role === "x-translation") {
+ line.translatedLyric = wordEl.innerHTML;
+ } else if (role === "x-roman") {
+ line.romanLyric = wordEl.innerHTML;
+ }
+ } else if (wordEl.hasAttribute("begin") && wordEl.hasAttribute("end")) {
+ const word: LyricWord = {
+ word: wordNode.textContent ?? "",
+ startTime: parseTimespan(wordEl.getAttribute("begin") ?? ""),
+ endTime: parseTimespan(wordEl.getAttribute("end") ?? ""),
+ };
+ const emptyBeat = wordEl.getAttribute("amll:empty-beat");
+ if (emptyBeat) {
+ word.emptyBeat = Number(emptyBeat);
+ }
+ line.words.push(word);
+ }
+ }
+ }
+
+ if (line.isBG) {
+ const firstWord = line.words?.[0];
+ if (firstWord?.word.startsWith("(")) {
+ firstWord.word = firstWord.word.substring(1);
+ if (firstWord.word.length === 0) {
+ line.words.shift();
+ }
+ }
+
+ const lastWord = line.words?.[line.words.length - 1];
+ if (lastWord?.word.endsWith(")")) {
+ lastWord.word = lastWord.word.substring(0, lastWord.word.length - 1);
+ if (lastWord.word.length === 0) {
+ line.words.pop();
+ }
+ }
+ }
+
+ const startTime = lineEl.getAttribute("begin");
+ const endTime = lineEl.getAttribute("end");
+ if (startTime && endTime) {
+ line.startTime = parseTimespan(startTime);
+ line.endTime = parseTimespan(endTime);
+ } else {
+ line.startTime = line.words
+ .filter((v) => v.word.trim().length > 0)
+ .reduce((pv, cv) => Math.min(pv, cv.startTime), Infinity);
+ line.endTime = line.words
+ .filter((v) => v.word.trim().length > 0)
+ .reduce((pv, cv) => Math.max(pv, cv.endTime), 0);
+ }
+
+ if (haveBg) {
+ const bgLine = lyricLines.pop();
+ lyricLines.push(line);
+ if (bgLine) lyricLines.push(bgLine);
+ } else {
+ lyricLines.push(line);
+ }
+ }
+
+ for (const lineEl of ttmlDoc.querySelectorAll("body p[begin][end]")) {
+ parseParseLine(lineEl);
+ }
+
+ return {
+ metadata,
+ lyricLines: lyricLines,
+ };
+}
diff --git a/src/lib/ttml/ttml-types.ts b/src/lib/ttml/ttml-types.ts
new file mode 100644
index 0000000..644a870
--- /dev/null
+++ b/src/lib/ttml/ttml-types.ts
@@ -0,0 +1,26 @@
+export interface TTMLMetadata {
+ key: string;
+ value: string[];
+}
+
+export interface TTMLLyric {
+ metadata: TTMLMetadata[];
+ lyricLines: LyricLine[];
+}
+
+export interface LyricWord {
+ startTime: number;
+ endTime: number;
+ word: string;
+ emptyBeat?: number;
+}
+
+export interface LyricLine {
+ words: LyricWord[];
+ translatedLyric: string;
+ romanLyric: string;
+ isBG: boolean;
+ isDuet: boolean;
+ startTime: number;
+ endTime: number;
+}
diff --git a/src/lib/ttml/writer.ts b/src/lib/ttml/writer.ts
new file mode 100644
index 0000000..30668a1
--- /dev/null
+++ b/src/lib/ttml/writer.ts
@@ -0,0 +1,260 @@
+/**
+ * @fileoverview
+ * 用于将内部歌词数组对象导出成 TTML 格式的模块
+ * 但是可能会有信息会丢失
+ */
+
+import type { LyricLine, LyricWord, TTMLLyric } from "./ttml-types";
+
+function msToTimestamp(timeMS: number): string {
+ let time = timeMS;
+ if (!Number.isSafeInteger(time) || time < 0) {
+ return "00:00.000";
+ }
+ if (time === Infinity) {
+ return "99:99.999";
+ }
+ time = time / 1000;
+ const secs = time % 60;
+ time = (time - secs) / 60;
+ const mins = time % 60;
+ const hrs = (time - mins) / 60;
+
+ const h = hrs.toString().padStart(2, "0");
+ const m = mins.toString().padStart(2, "0");
+ const s = secs.toFixed(3).padStart(6, "0");
+
+ if (hrs > 0) {
+ return `${h}:${m}:${s}`;
+ }
+ return `${m}:${s}`;
+}
+
+export function exportTTML(ttmlLyric: TTMLLyric, pretty = false): string {
+ const params: LyricLine[][] = [];
+ const lyric = ttmlLyric.lyricLines;
+
+ let tmp: LyricLine[] = [];
+ for (const line of lyric) {
+ if (line.words.length === 0 && tmp.length > 0) {
+ params.push(tmp);
+ tmp = [];
+ } else {
+ tmp.push(line);
+ }
+ }
+
+ if (tmp.length > 0) {
+ params.push(tmp);
+ }
+
+ const doc = new Document();
+
+ function createWordElement(word: LyricWord): Element {
+ const span = doc.createElement("span");
+ span.setAttribute("begin", msToTimestamp(word.startTime));
+ span.setAttribute("end", msToTimestamp(word.endTime));
+ if (word.emptyBeat) {
+ span.setAttribute("amll:empty-beat", `${word.emptyBeat}`);
+ }
+ span.appendChild(doc.createTextNode(word.word));
+ return span;
+ }
+
+ const ttRoot = doc.createElement("tt");
+
+ ttRoot.setAttribute("xmlns", "http://www.w3.org/ns/ttml");
+ ttRoot.setAttribute("xmlns:ttm", "http://www.w3.org/ns/ttml#metadata");
+ ttRoot.setAttribute("xmlns:amll", "http://www.example.com/ns/amll");
+ ttRoot.setAttribute(
+ "xmlns:itunes",
+ "http://music.apple.com/lyric-ttml-internal",
+ );
+
+ doc.appendChild(ttRoot);
+
+ const head = doc.createElement("head");
+
+ ttRoot.appendChild(head);
+
+ const body = doc.createElement("body");
+ const hasOtherPerson = !!lyric.find((v) => v.isDuet);
+
+ const metadataEl = doc.createElement("metadata");
+ const mainPersonAgent = doc.createElement("ttm:agent");
+ mainPersonAgent.setAttribute("type", "person");
+ mainPersonAgent.setAttribute("xml:id", "v1");
+
+ metadataEl.appendChild(mainPersonAgent);
+
+ if (hasOtherPerson) {
+ const otherPersonAgent = doc.createElement("ttm:agent");
+ otherPersonAgent.setAttribute("type", "other");
+ otherPersonAgent.setAttribute("xml:id", "v2");
+
+ metadataEl.appendChild(otherPersonAgent);
+ }
+
+ for (const metadata of ttmlLyric.metadata) {
+ for (const value of metadata.value) {
+ const metaEl = doc.createElement("amll:meta");
+ metaEl.setAttribute("key", metadata.key);
+ metaEl.setAttribute("value", value);
+ metadataEl.appendChild(metaEl);
+ }
+ }
+
+ head.appendChild(metadataEl);
+
+ let i = 0;
+
+ const guessDuration = lyric[lyric.length - 1]?.endTime ?? 0;
+ body.setAttribute("dur", msToTimestamp(guessDuration));
+
+ for (const param of params) {
+ const paramDiv = doc.createElement("div");
+ const beginTime = param[0]?.startTime ?? 0;
+ const endTime = param[param.length - 1]?.endTime ?? 0;
+
+ paramDiv.setAttribute("begin", msToTimestamp(beginTime));
+ paramDiv.setAttribute("end", msToTimestamp(endTime));
+
+ for (let lineIndex = 0; lineIndex < param.length; lineIndex++) {
+ const line = param[lineIndex];
+ const lineP = doc.createElement("p");
+ const beginTime = line.startTime ?? 0;
+ const endTime = line.endTime;
+
+ lineP.setAttribute("begin", msToTimestamp(beginTime));
+ lineP.setAttribute("end", msToTimestamp(endTime));
+
+ lineP.setAttribute("ttm:agent", line.isDuet ? "v2" : "v1");
+ lineP.setAttribute("itunes:key", `L${++i}`);
+
+ if (line.words.length > 1) {
+ let beginTime = Infinity;
+ let endTime = 0;
+ for (const word of line.words) {
+ if (word.word.trim().length === 0) {
+ lineP.appendChild(doc.createTextNode(word.word));
+ } else {
+ const span = createWordElement(word);
+ lineP.appendChild(span);
+ beginTime = Math.min(beginTime, word.startTime);
+ endTime = Math.max(endTime, word.endTime);
+ }
+ }
+ lineP.setAttribute("begin", msToTimestamp(line.startTime));
+ lineP.setAttribute("end", msToTimestamp(line.endTime));
+ } else if (line.words.length === 1) {
+ const word = line.words[0];
+ lineP.appendChild(doc.createTextNode(word.word));
+ lineP.setAttribute("begin", msToTimestamp(word.startTime));
+ lineP.setAttribute("end", msToTimestamp(word.endTime));
+ }
+
+ const nextLine = param[lineIndex + 1];
+ if (nextLine?.isBG) {
+ lineIndex++;
+ const bgLine = nextLine;
+ const bgLineSpan = doc.createElement("span");
+ bgLineSpan.setAttribute("ttm:role", "x-bg");
+
+ if (bgLine.words.length > 1) {
+ let beginTime = Infinity;
+ let endTime = 0;
+ for (
+ let wordIndex = 0;
+ wordIndex < bgLine.words.length;
+ wordIndex++
+ ) {
+ const word = bgLine.words[wordIndex];
+ if (word.word.trim().length === 0) {
+ bgLineSpan.appendChild(doc.createTextNode(word.word));
+ } else {
+ const span = createWordElement(word);
+ if (wordIndex === 0) {
+ span.prepend(doc.createTextNode("("));
+ } else if (wordIndex === bgLine.words.length - 1) {
+ span.appendChild(doc.createTextNode(")"));
+ }
+ bgLineSpan.appendChild(span);
+ beginTime = Math.min(beginTime, word.startTime);
+ endTime = Math.max(endTime, word.endTime);
+ }
+ }
+ bgLineSpan.setAttribute("begin", msToTimestamp(beginTime));
+ bgLineSpan.setAttribute("end", msToTimestamp(endTime));
+ } else if (bgLine.words.length === 1) {
+ const word = bgLine.words[0];
+ bgLineSpan.appendChild(doc.createTextNode(`(${word.word})`));
+ bgLineSpan.setAttribute("begin", msToTimestamp(word.startTime));
+ bgLineSpan.setAttribute("end", msToTimestamp(word.endTime));
+ }
+
+ if (bgLine.translatedLyric) {
+ const span = doc.createElement("span");
+ span.setAttribute("ttm:role", "x-translation");
+ span.setAttribute("xml:lang", "zh-CN");
+ span.appendChild(doc.createTextNode(bgLine.translatedLyric));
+ bgLineSpan.appendChild(span);
+ }
+
+ if (bgLine.romanLyric) {
+ const span = doc.createElement("span");
+ span.setAttribute("ttm:role", "x-roman");
+ span.appendChild(doc.createTextNode(bgLine.romanLyric));
+ bgLineSpan.appendChild(span);
+ }
+
+ lineP.appendChild(bgLineSpan);
+ }
+
+ if (line.translatedLyric) {
+ const span = doc.createElement("span");
+ span.setAttribute("ttm:role", "x-translation");
+ span.setAttribute("xml:lang", "zh-CN");
+ span.appendChild(doc.createTextNode(line.translatedLyric));
+ lineP.appendChild(span);
+ }
+
+ if (line.romanLyric) {
+ const span = doc.createElement("span");
+ span.setAttribute("ttm:role", "x-roman");
+ span.appendChild(doc.createTextNode(line.romanLyric));
+ lineP.appendChild(span);
+ }
+
+ paramDiv.appendChild(lineP);
+ }
+
+ body.appendChild(paramDiv);
+ }
+
+ ttRoot.appendChild(body);
+
+ if (pretty) {
+ const xsltDoc = new DOMParser().parseFromString(
+ [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ " ",
+ ' ',
+ ' ',
+ " ",
+ ' ',
+ "",
+ ].join("\n"),
+ "application/xml",
+ );
+
+ const xsltProcessor = new XSLTProcessor();
+ xsltProcessor.importStylesheet(xsltDoc);
+ const resultDoc = xsltProcessor.transformToDocument(doc);
+
+ return new XMLSerializer().serializeToString(resultDoc);
+ }
+ return new XMLSerializer().serializeToString(doc);
+}
diff --git a/src/routes/import/[id]/lyric/+page.svelte b/src/routes/import/[id]/lyric/+page.svelte
index a52a1f8..192174c 100644
--- a/src/routes/import/[id]/lyric/+page.svelte
+++ b/src/routes/import/[id]/lyric/+page.svelte
@@ -16,7 +16,7 @@
歌词文件
-
+
diff --git a/src/routes/play/[id]/+page.svelte b/src/routes/play/[id]/+page.svelte
index b7558eb..514b51e 100644
--- a/src/routes/play/[id]/+page.svelte
+++ b/src/routes/play/[id]/+page.svelte
@@ -4,15 +4,19 @@
import Background from '$lib/components/background.svelte';
import Cover from '$lib/components/cover.svelte';
import InteractiveBox from '$lib/components/interactiveBox.svelte';
- import Lyrics from '$lib/components/lyrics.svelte';
import extractFileName from '$lib/extractFileName';
import localforage from 'localforage';
import { writable } from 'svelte/store';
- import lrcParser, { type LrcJsonData } from '$lib/lyrics/parser';
import userAdjustingProgress from '$lib/state/userAdjustingProgress';
import type { IAudioMetadata } from 'music-metadata-browser';
- import { onMount } from 'svelte';
+ 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;
@@ -25,9 +29,9 @@
let paused: boolean = true;
let launched = false;
let prepared: string[] = [];
- let originalLyrics: LrcJsonData;
- let lyricsText: string[] = [];
+ let lyricLines: LyricLine[];
let hasLyrics: boolean;
+ let lyricPlayer: LyricPlayer = new CoreLyricPlayer();
const coverPath = writable('');
let mainInterval: ReturnType;
@@ -44,26 +48,26 @@
]
});
ms.setActionHandler('play', function () {
- if (audioPlayer===null) return;
+ if (audioPlayer === null) return;
audioPlayer.play();
paused = false;
});
ms.setActionHandler('pause', function () {
- if (audioPlayer===null) return;
+ if (audioPlayer === null) return;
audioPlayer.pause();
paused = true;
});
ms.setActionHandler('seekbackward', function () {
- if (audioPlayer===null) return;
+ if (audioPlayer === null) return;
if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0;
}
});
ms.setActionHandler('previoustrack', function () {
- if (audioPlayer===null) return;
+ if (audioPlayer === null) return;
if (audioPlayer.currentTime > 4) {
audioPlayer.currentTime = 0;
}
@@ -85,7 +89,7 @@
prepared.push('cover');
});
localforage.getItem(`${audioId}-file`, function (err, file) {
- if (audioPlayer===null) return;
+ if (audioPlayer === null) return;
if (file) {
const f = file as File;
audioFile = f;
@@ -99,10 +103,26 @@
if (file) {
const f = file as File;
f.text().then((lr) => {
- originalLyrics = lrcParser(lr);
- if (!originalLyrics.scripts) return;
- for (const line of originalLyrics.scripts) {
- lyricsText.push(line.text);
+ 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;
}
});
}
@@ -110,7 +130,7 @@
}
function playAudio() {
- if (audioPlayer===null) return;
+ if (audioPlayer === null) return;
if (audioPlayer.duration) {
duration = audioPlayer.duration;
}
@@ -140,6 +160,7 @@
if (audioPlayer) {
audioPlayer.currentTime = duration * progress;
currentProgress = duration * progress;
+ lyricPlayer.calcLayout(false, true);
}
}
@@ -158,17 +179,19 @@
$: {
clearInterval(mainInterval);
mainInterval = setInterval(() => {
- if (audioPlayer===null) return;
- if ($userAdjustingProgress === false)
- currentProgress = audioPlayer.currentTime;
+ 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;
- });
+ onMount(() => {
+ if (audioPlayer === null) return;
+ audioPlayer.volume = localStorage.getItem('volume') ? Number(localStorage.getItem('volume')) : 1;
+ });
+ onDestroy(() => {
+ if (audioPlayer === null) return;
+ });
$: {
if (audioPlayer) {
@@ -177,7 +200,10 @@
}
}
- $: hasLyrics = !!originalLyrics;
+ function onLyricLineClick(e: LyricLineMouseEvent) {
+ lyricPlayer.resetScroll();
+ adjustProgress(lyricLines[e.lineIndex].startTime / 1000 / duration);
+ }
readDB();
@@ -202,18 +228,24 @@
{hasLyrics}
/>
-
+