[+] Option to dedup lines

This commit is contained in:
2025-11-21 02:50:01 +08:00
parent e5d6a8ae20
commit 917df282e8
3 changed files with 39 additions and 12 deletions
+22 -10
View File
@@ -5,13 +5,13 @@
import { onMount, tick } from "svelte";
import { typingSettingsDefault, type LyricSegment } from "../../../shared/types.ts";
import { isKana, isKanji, toHiragana, toKatakana, toRomaji } from "wanakana";
import { composeList, fuzzyEquals, processLrcLine, type ProcLrcLine, type ProcLrcSeg } from "./IMEHelper.ts";
import { composeList, fuzzyEquals, processLrcLine, dedupLines, type ProcLrcLine, type ProcLrcSeg } from "./IMEHelper.ts";
import MenuItem from "../../../components/material3/MenuItem.svelte";
import "../../../shared/ext.ts"
import { API } from "$lib/client.ts";
import { animateCaret } from "./animation.ts";
import { goto } from '$app/navigation';
import { artistAndAlbum } from "../../../shared/tools.ts";
import { artistAndAlbum } from "../../../shared/tools.ts";
let { data }: PageProps = $props()
@@ -34,11 +34,21 @@
const preprocessKana = (kana: string, state?: string) => (settings.showRomaji || (settings.showRomajiOnError && state === 'wrong')) ? `<ruby>${_preprocessKana(kana)}<rt>${toRomaji(kana)}</rt></ruby>` : _preprocessKana(kana)
// Process each line into segments with swi (start word index) and kanji/kana
let processedLrc: ProcLrcLine[] = data.lrc.map(line => processLrcLine(line.lyric))
let processedLrc: ProcLrcLine[] = $derived(dedupLines(data.lrc, settings.hideRepeated).map(line => processLrcLine(line.lyric)))
// State tracking for each kana character: UNSEEN, RIGHT, WRONG
let states = $state(processedLrc.map(line => new Array(line.totalLen).fill('unseen')))
const allStates = (l: number, seg: ProcLrcSeg) => states[l].slice(seg.swi, seg.swi + seg.kana.length)
let states = $state(dedupLines(data.lrc, settings.hideRepeated).map(line => processLrcLine(line.lyric)).map(line => new Array(line.totalLen).fill('unseen')))
$effect(() => {
// Reset when processedLrc changes (settings changed)
states = processedLrc.map(line => new Array(line.totalLen).fill('unseen'))
li = 0
wi = 0
inp = ""
startTime = 0
statsHistory = []
})
const allStates = (l: number, seg: ProcLrcSeg) => states[l]?.slice(seg.swi, seg.swi + seg.kana.length) ?? []
const getKanjiState = (l: number, seg: ProcLrcSeg) => {
let sts = allStates(l, seg)
if (sts.every(s => s === 'right')) return 'right'
@@ -72,6 +82,7 @@
// On input changed: Convert to hiragana, compare with current position, update states
function inputChanged(input: string, isComposed: boolean) {
if (!processedLrc[li] || !states[li]) return;
if (!startTime && input) startTime = Date.now()
console.log(`input changed: ${input}`)
// Convert to hiragana
@@ -125,7 +136,6 @@
inp = ""
}
}
$effect(() => inputChanged(inp, false))
// Caret: Typing indicator
@@ -175,6 +185,7 @@
<MenuItem textIcon="カ" onclick={() => settings.allKata = !settings.allKata}>{settings.allKata ? "恢复平假名" : "全部转换为片假名"}</MenuItem>
<MenuItem icon="i-material-symbols:language-japanese-kana-rounded" onclick={() => settings.showRomaji = !settings.showRomaji}>{settings.showRomaji ? "隐藏罗马音" : "显示罗马音"}</MenuItem>
<MenuItem icon="i-material-symbols:error-circle-rounded" onclick={() => settings.showRomajiOnError = !settings.showRomajiOnError}>{settings.showRomajiOnError ? "不在错误时显示罗马音" : "错误时显示罗马音"}</MenuItem>
<MenuItem icon="i-material-symbols:compress-rounded" onclick={() => settings.hideRepeated = !settings.hideRepeated}>{settings.hideRepeated ? "显示重复行" : "隐藏重复行"}</MenuItem>
{#if loc}
<MenuItem icon={loc.playMode === 'random' ? "i-material-symbols:shuffle-rounded" : "i-material-symbols:repeat-rounded"} onclick={() =>
loc.playMode = loc.playMode === 'random' ? 'sequential' : 'random'}>{loc.playMode === 'random' ? "当前:随机播放" : "当前:顺序播放"}</MenuItem>
@@ -211,16 +222,16 @@
{#each line.parts as seg}
{#if !seg.kanji}
{#each seg.kana as char, c}
<span class="{states[l][seg.swi + c]}" class:here={l === li && wi === seg.swi + c}
<span class="{states[l]?.[seg.swi + c] ?? ''}" class:here={l === li && wi === seg.swi + c}
class:punctuation={!isKana(char) && !isKanji(char)}>
{@html preprocessKana(char, states[l][seg.swi + c])}
{@html preprocessKana(char, states[l]?.[seg.swi + c])}
</span>
{/each}
{:else}
<ruby>
<span class="{getKanjiState(l, seg)}">{seg.kanji}</span>{#if settings.isFuri}<rt>
{#each seg.kana as char, c}
<span class="{states[l][seg.swi + c]}" class:here={l === li && wi === seg.swi + c}>{@html preprocessKana(char, states[l][seg.swi + c])}</span>
<span class="{states[l]?.[seg.swi + c] ?? ''}" class:here={l === li && wi === seg.swi + c}>{@html preprocessKana(char, states[l]?.[seg.swi + c])}</span>
{/each}
</rt>{/if}
</ruby>
@@ -228,6 +239,7 @@
{/each}
</div>
{/each}
<div class="h-30vh"></div>
</div>
</div>
+16 -2
View File
@@ -1,5 +1,5 @@
import { toHiragana } from "wanakana";
import type { LyricSegment } from "../../../shared/types";
import type { LyricLine, LyricSegment } from "../../../shared/types";
export type ProcLrcSeg = { swi: number, kanji?: string, kana: string }
export type ProcLrcLine = { parts: ProcLrcSeg[], totalLen: number }
@@ -33,4 +33,18 @@ export const composeList = [
'だ', 'ぢ', 'づ', 'で', 'ど',
'ば', 'び', 'ぶ', 'べ', 'ぼ',
'ぱ', 'ぴ', 'ぷ', 'ぺ', 'ぽ'
]
]
/**
* Remove duplicate lyric lines based on their content.
*/
export function dedupLines(lrc: LyricLine[], hide: boolean) {
if (!hide) return lrc;
const seen = new Set<string>();
return lrc.filter(line => {
const key = JSON.stringify(line.lyric);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
+1
View File
@@ -87,6 +87,7 @@ export const typingSettingsDefault = {
allKata: false,
showRomaji: true,
showRomajiOnError: true,
hideRepeated: false,
};
export interface UserData {