From f70fd90032ccd1c935fa95b0bbe7e9477720809a Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 09:31:44 +0800 Subject: [PATCH 1/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a488d5..277da33 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Practice Japanese Karaoke lyrics reading and typing at the same time with KaraDa ## Technical Tasks * [x] i18n -* [ ] 404 page +* [x] 404 page * [ ] Previous song / next song buttons * [ ] Update an existing playlist * [ ] Allow users to correct lyric pronunciations through correction feedback From f6ba9a8897c6dc07880d666f5966fe8390ad48c0 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:09:37 +0800 Subject: [PATCH 2/5] [+] Next song button --- src/lib/ui/player/PlayerAppBar.svelte | 34 ++++++++++++++++++-- src/lib/ui/player/SongSwitching.ts | 29 +++++++++++++++++ src/routes/results/[id]/+page.svelte | 27 +++------------- src/routes/song/[id]/+layout.server.ts | 10 ++++++ src/routes/song/[id]/+page.server.ts | 6 ---- src/routes/song/[id]/+page.svelte | 15 ++++++--- src/routes/song/[id]/karaoke/+page.server.ts | 5 ++- src/routes/song/[id]/karaoke/+page.svelte | 5 ++- src/routes/song/[id]/play/+page.server.ts | 5 ++- src/routes/song/[id]/play/+page.svelte | 2 +- 10 files changed, 93 insertions(+), 45 deletions(-) create mode 100644 src/lib/ui/player/SongSwitching.ts create mode 100644 src/routes/song/[id]/+layout.server.ts delete mode 100644 src/routes/song/[id]/+page.server.ts diff --git a/src/lib/ui/player/PlayerAppBar.svelte b/src/lib/ui/player/PlayerAppBar.svelte index 1db357e..791d740 100644 --- a/src/lib/ui/player/PlayerAppBar.svelte +++ b/src/lib/ui/player/PlayerAppBar.svelte @@ -2,12 +2,16 @@ import AppBar from "$lib/ui/appbar/AppBar.svelte" import MenuItem from "$lib/ui/material3/MenuItem.svelte" import { artistAndAlbum } from "$lib/utils" - import type { TypingSettings, UserData, NeteaseSong } from "$lib/types" + import type { TypingSettings, UserData, NeteaseSong, NeteasePlaylist } from "$lib/types" + import { goto } from "$app/navigation" + import { API } from "$lib/client" + import { getNextSong, getNextLoc } from "./SongSwitching" interface Props { song: NeteaseSong settings: TypingSettings loc?: UserData['loc'] + playlist?: NeteasePlaylist showRomajiOnError?: boolean disableHideRepeated?: boolean isKaraoke?: boolean @@ -17,12 +21,30 @@ song, settings = $bindable(), loc = $bindable(), + playlist, showRomajiOnError = true, disableHideRepeated = false, isKaraoke = false }: Props = $props() let isHideRepeated = $derived(settings.hideRepeated && !disableHideRepeated) + + const nextSongId = $derived(getNextSong(playlist, loc)) + async function handleNext() { + if (!loc || !playlist) return + + if (nextSongId) { + const newLoc = getNextLoc(playlist, loc, nextSongId) + loc = newLoc // Update local state + await API.saveUserData({ loc }) + goto(`/song/${nextSongId}`, { replaceState: true }) + } else { + // Playlist finished + loc.isFinished = true + await API.saveUserData({ loc }) + goto(`/playlist/${playlist.id}`) + } + } @@ -40,7 +62,13 @@ onclick={() => settings.hideRepeated = !settings.hideRepeated}>{isHideRepeated ? "显示重复行" : "隐藏重复行"} {#if loc} - - loc.playMode = loc.playMode === 'random' ? 'sequential' : 'random'}>{loc.playMode === 'random' ? "当前:随机播放" : "当前:顺序播放"} + loc!.playMode = loc!.playMode === 'random' ? 'sequential' : 'random'}> + {loc.playMode === 'random' ? "当前:随机播放" : "当前:顺序播放"} + + + {#if nextSongId} + 下首 + {/if} {/if} diff --git a/src/lib/ui/player/SongSwitching.ts b/src/lib/ui/player/SongSwitching.ts new file mode 100644 index 0000000..60c2d71 --- /dev/null +++ b/src/lib/ui/player/SongSwitching.ts @@ -0,0 +1,29 @@ +import type { NeteasePlaylist, UserData } from "$lib/types" + +export function getNextSong(playlist?: NeteasePlaylist, loc?: NonNullable) { + if (!playlist || !loc) return null + if (loc.playMode === 'random') { + const unplayed = playlist.tracks.filter(t => !loc.playedSongIds.includes(t.id)) + if (unplayed.length > 0) { + return unplayed[Math.floor(Math.random() * unplayed.length)].id + } + } else { + const nextIndex = loc.currentSongIndex + 1 + if (nextIndex < playlist.tracks.length) { + return playlist.tracks[nextIndex].id + } + } + return null +} + +export function getNextLoc(playlist: NeteasePlaylist, loc: NonNullable, nextSongId: number): NonNullable { + const nextIndex = playlist.tracks.findIndex(t => t.id === nextSongId) + return { + ...loc, + currentSongIndex: nextIndex, + isFinished: false, + playedSongIds: loc.playedSongIds.includes(nextSongId) + ? loc.playedSongIds + : [...loc.playedSongIds, nextSongId] + } +} diff --git a/src/routes/results/[id]/+page.svelte b/src/routes/results/[id]/+page.svelte index b19abbd..cb12507 100644 --- a/src/routes/results/[id]/+page.svelte +++ b/src/routes/results/[id]/+page.svelte @@ -8,6 +8,7 @@ import Chart from "chart.js/auto" import { API } from "$lib/client" import { getI18n } from "$lib/i18n" + import { getNextSong, getNextLoc } from "$lib/ui/player/SongSwitching" const t = getI18n().results @@ -105,35 +106,15 @@ // Compute next state immediately if (playlist && loc && isCurrentResult) { - if (loc.playMode === 'random') { - const unplayed = playlist.tracks.filter((t: NeteaseSong) => !loc.playedSongIds.includes(t.id)) - if (unplayed.length > 0) { - const nextSong = unplayed[Math.floor(Math.random() * unplayed.length)] - nextSongId = nextSong.id - } else isPlaylistFinished = true - } else { - const nextIndex = loc.currentSongIndex + 1 - if (nextIndex < playlist.tracks.length) { - nextSongId = playlist.tracks[nextIndex].id - } else isPlaylistFinished = true - } + nextSongId = getNextSong(playlist, loc) + if (nextSongId === null) isPlaylistFinished = true } async function handleNext() { if (nextSongId !== null) { if (!data.user.data.loc || !data.playlist) return - const nextIndex = data.playlist.tracks.findIndex((t: NeteaseSong) => t.id === nextSongId) - - const newLoc = { - ...data.user.data.loc, - currentSongIndex: nextIndex, - isFinished: false - } - - if (!newLoc.playedSongIds.includes(nextSongId)) { - newLoc.playedSongIds = [...newLoc.playedSongIds, nextSongId] - } + const newLoc = getNextLoc(data.playlist, data.user.data.loc, nextSongId) data.user.data.loc = newLoc await API.saveUserData({ loc: newLoc }) diff --git a/src/routes/song/[id]/+layout.server.ts b/src/routes/song/[id]/+layout.server.ts new file mode 100644 index 0000000..7b67422 --- /dev/null +++ b/src/routes/song/[id]/+layout.server.ts @@ -0,0 +1,10 @@ +import { getSongRaw, getPlaylist } from "$lib/server/songs" + +export const load = async ({ params, parent }) => { + const { user } = await parent() + const songId = +params.id + const song = await getSongRaw(songId) + const playlist = await user.data?.loc?.currentPlaylistId?.let(getPlaylist) + + return { song, playlist } +} diff --git a/src/routes/song/[id]/+page.server.ts b/src/routes/song/[id]/+page.server.ts deleted file mode 100644 index f51ca31..0000000 --- a/src/routes/song/[id]/+page.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getSongRaw } from "$lib/server/songs" - -export const load = async ({ params }) => { - const song = await getSongRaw(+params.id) - return { song } -} diff --git a/src/routes/song/[id]/+page.svelte b/src/routes/song/[id]/+page.svelte index be9b7ed..3b56631 100644 --- a/src/routes/song/[id]/+page.svelte +++ b/src/routes/song/[id]/+page.svelte @@ -2,15 +2,23 @@ import { API } from "$lib/client" import { onMount } from "svelte" import Button from "$lib/ui/Button.svelte" - import AppBar from "$lib/ui/appbar/AppBar.svelte" + import PlayerAppBar from "$lib/ui/player/PlayerAppBar.svelte" import ProgressList from "$lib/ui/ProgressList.svelte" import { goto } from "$app/navigation" import { artistAndAlbum } from "$lib/utils" import { getI18n } from "$lib/i18n" + import { typingSettingsDefault } from "$lib/types" const t = getI18n().song.mode let { data } = $props() + + let settings = $state(data.user.data?.typingSettings ?? typingSettingsDefault) + $effect(() => { API.saveUserData({ typingSettings: settings }) }) + + let loc = $state(data.user.data.loc) + $effect(() => { API.saveUserData({ loc }) }) + let taskStatus = $state({ lyrics: false, ai: false, @@ -24,7 +32,6 @@ { icon: "i-material-symbols:mic-rounded", label: t.karaoke, url: `/song/${data.song.id}/karaoke`, disabled: !taskStatus.separation }, ]) - let loadStatus = $state<"idle" | "loading" | "done">("idle") let progressItems = $state([]) let progressPercentage = $state(0) @@ -33,7 +40,6 @@ }) async function startLoading() { - loadStatus = "loading" await API.song.prepare(data.song.id) const interval = setInterval(async () => { const res = await API.song.status(data.song.id) @@ -62,7 +68,6 @@ if (state.status === "done") { clearInterval(interval) - loadStatus = "done" progressPercentage = 100 } else if (state.status === "error") { clearInterval(interval) @@ -71,7 +76,7 @@ } - + diff --git a/src/routes/song/[id]/karaoke/+page.server.ts b/src/routes/song/[id]/karaoke/+page.server.ts index a6bca0a..cf69650 100644 --- a/src/routes/song/[id]/karaoke/+page.server.ts +++ b/src/routes/song/[id]/karaoke/+page.server.ts @@ -1,10 +1,9 @@ import type { PageServerLoad } from './$types' -import { getLyricsProcessed, getSongRaw, getSongUrl, checkLyricsProcessed } from "$lib/server/songs.ts" +import { getLyricsProcessed, getSongUrl, checkLyricsProcessed } from "$lib/server/songs.ts" import { redirect } from '@sveltejs/kit' export const load: PageServerLoad = async ({ params }) => { const songId = +params.id - const song = await getSongRaw(songId) const hasLrc = await checkLyricsProcessed(songId) if (!hasLrc) throw redirect(302, `/song/${songId}`) @@ -12,5 +11,5 @@ export const load: PageServerLoad = async ({ params }) => { const lrc = await getLyricsProcessed(songId)! const audioData = await getSongUrl(songId) - return { song, lrc, audioData } + return { lrc, audioData } } diff --git a/src/routes/song/[id]/karaoke/+page.svelte b/src/routes/song/[id]/karaoke/+page.svelte index dbfad3e..dcee018 100644 --- a/src/routes/song/[id]/karaoke/+page.svelte +++ b/src/routes/song/[id]/karaoke/+page.svelte @@ -18,6 +18,9 @@ let settings = $state(data.user.data?.typingSettings ?? typingSettingsDefault) $effect(() => { API.saveUserData({ typingSettings: settings }) }) + let loc = $state(data.user.data.loc) + $effect(() => { API.saveUserData({ loc }) }) + let vocalsVolume = $state(100) // 0-100 // Process lyrics @@ -79,7 +82,7 @@ musicControl?.ready()} onkeydown={() => musicControl?.ready()}/> - +
{#if data.audioData.vocalsUrl} diff --git a/src/routes/song/[id]/play/+page.server.ts b/src/routes/song/[id]/play/+page.server.ts index 3488281..0cdde3a 100644 --- a/src/routes/song/[id]/play/+page.server.ts +++ b/src/routes/song/[id]/play/+page.server.ts @@ -1,15 +1,14 @@ import type { PageServerLoad } from './$types' -import { getLyricsProcessed, getSongRaw, getSongUrl, checkLyricsProcessed } from "$lib/server/songs.ts" +import { getLyricsProcessed, getSongUrl, checkLyricsProcessed } from "$lib/server/songs.ts" import { redirect } from '@sveltejs/kit' export const load: PageServerLoad = async ({ params, url }) => { const songId = +params.id - const song = await getSongRaw(songId) const hasLrc = await checkLyricsProcessed(songId) if (!hasLrc) throw redirect(302, `/song/${songId}`) const lrc = await getLyricsProcessed(songId)! const audioUrl = url.searchParams.get('music') === 'true' ? await getSongUrl(songId) : undefined - return { song, lrc, audioUrl } + return { lrc, audioUrl } } \ No newline at end of file diff --git a/src/routes/song/[id]/play/+page.svelte b/src/routes/song/[id]/play/+page.svelte index 0ee28d8..1572193 100644 --- a/src/routes/song/[id]/play/+page.svelte +++ b/src/routes/song/[id]/play/+page.svelte @@ -157,7 +157,7 @@ musicControl?.ready()} onkeydown={() => musicControl?.ready()}/> - + From 3bb4ff8e9a2dc8ce32a0fd15287b28a3825024e7 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:09:55 +0800 Subject: [PATCH 3/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 277da33..bac48ac 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Practice Japanese Karaoke lyrics reading and typing at the same time with KaraDa * [x] i18n * [x] 404 page -* [ ] Previous song / next song buttons +* [x] Previous song / next song buttons * [ ] Update an existing playlist * [ ] Allow users to correct lyric pronunciations through correction feedback * [ ] Correct lyrics timing inconsistencies (i.e. 网易云的歌词因为是业余用户上传的,时间戳不一定准确。但是 waveform 里面可以分析出每句歌词的具体开始结束时间,也许可以自动修正) From 44d99dc8c1e9113c0ebf281b715ab2009f31fdb1 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:32:09 +0800 Subject: [PATCH 4/5] [+] Update playlist option --- src/lib/i18n/en.ts | 4 +++- src/lib/i18n/ja.ts | 4 +++- src/lib/i18n/zh.ts | 4 +++- src/lib/server/songs.ts | 2 +- src/lib/ui/ProgressList.svelte | 8 ++++---- src/routes/import/netease/+page.svelte | 6 ++++-- src/routes/playlist/[id]/+page.svelte | 8 +++++--- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index 39dbdbe..47f1017 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -37,6 +37,7 @@ export default { tip: 'Go to NetEase Music App, find a Japanese playlist you like, click share, copy link, and paste it here to start importing!', inputLabel: 'NetEase Playlist Link / ID', btnStart: 'Start Import', + btnUpdate: 'Update Playlist', btnView: 'View Playlist' } }, @@ -47,7 +48,8 @@ export default { count: 'Songs: ', startPractice: 'Start Practice', songList: 'Song List', - songs: 'songs' + songs: 'songs', + updateFromNetease: 'Update from NetEase' }, list: { mine: 'My Playlists', diff --git a/src/lib/i18n/ja.ts b/src/lib/i18n/ja.ts index ca2e986..425a73c 100644 --- a/src/lib/i18n/ja.ts +++ b/src/lib/i18n/ja.ts @@ -37,6 +37,7 @@ export default { tip: 'NetEase Musicアプリでお気に入りの日本語プレイリストを見つけ、共有をクリックし、リンクをコピーしてここに貼り付けると、インポートを開始できます!', inputLabel: 'NetEaseプレイリストリンク / ID', btnStart: 'インポート開始', + btnUpdate: 'プレイリストを更新', btnView: 'プレイリストを表示' } }, @@ -47,7 +48,8 @@ export default { count: '曲数: ', startPractice: '練習開始', songList: '曲リスト', - songs: '曲' + songs: '曲', + updateFromNetease: 'NetEaseから更新' }, list: { mine: 'マイプレイリスト', diff --git a/src/lib/i18n/zh.ts b/src/lib/i18n/zh.ts index 06d423f..c0e63ab 100644 --- a/src/lib/i18n/zh.ts +++ b/src/lib/i18n/zh.ts @@ -37,6 +37,7 @@ export default { tip: '去网易云 APP 找一个你喜欢的日本语歌单,点击分享,再点击复制链接,然后把链接粘贴到这里就可以开始导入了!', inputLabel: '网易云歌单链接 / ID', btnStart: '开始导入', + btnUpdate: '更新歌单', btnView: '查看歌单' } }, @@ -47,7 +48,8 @@ export default { count: '歌曲数: ', startPractice: '开始练习', songList: '歌曲列表', - songs: '首歌曲' + songs: '首歌曲', + updateFromNetease: '从网易云更新歌单' }, list: { mine: '我的歌单', diff --git a/src/lib/server/songs.ts b/src/lib/server/songs.ts index fbc00de..90dc354 100644 --- a/src/lib/server/songs.ts +++ b/src/lib/server/songs.ts @@ -226,7 +226,7 @@ export const getSession = (id: string) => sessions.get(id) * @returns Import session */ export async function startImport(link: string, userId?: number): Promise { - const meta = await getPlaylistRaw(parsePlaylistRef(link)) + const meta = await getPlaylistRaw(parsePlaylistRef(link), true) const importId = crypto.randomUUID() const session: ImportSession = { diff --git a/src/lib/ui/ProgressList.svelte b/src/lib/ui/ProgressList.svelte index b36d314..a5220bd 100644 --- a/src/lib/ui/ProgressList.svelte +++ b/src/lib/ui/ProgressList.svelte @@ -27,14 +27,14 @@ -
+
{#each items as item}
-
- {item.title} +
+ {item.title} {#if item.subtitle} - {item.subtitle} + {item.subtitle} {/if}
diff --git a/src/routes/import/netease/+page.svelte b/src/routes/import/netease/+page.svelte index 95b96ce..dac97af 100644 --- a/src/routes/import/netease/+page.svelte +++ b/src/routes/import/netease/+page.svelte @@ -7,10 +7,12 @@ import ErrorDialog from "$lib/ui/status/ErrorDialog.svelte" import ProgressList from "$lib/ui/ProgressList.svelte" import { getI18n } from "$lib/i18n" + import { page } from '$app/state' const t = getI18n().import.netease - let link = $state('') + let link = $state(page.url.searchParams.get('id') ?? '') + let isUpdate = $derived(!!page.url.searchParams.get('id')) interface SongImportStatus { song: NeteaseSong @@ -91,7 +93,7 @@
{#if status === 'idle'} - + {:else if status === 'success'} diff --git a/src/routes/playlist/[id]/+page.svelte b/src/routes/playlist/[id]/+page.svelte index a149b03..bf0ced4 100644 --- a/src/routes/playlist/[id]/+page.svelte +++ b/src/routes/playlist/[id]/+page.svelte @@ -6,6 +6,7 @@ import SongInfo from "$lib/ui/listitem/SongInfo.svelte" import { API } from "$lib/client" import { getI18n } from "$lib/i18n" + import MenuItem from "$lib/ui/material3/MenuItem.svelte"; const t = getI18n().playlist.detail @@ -49,9 +50,10 @@ { icon: isFavorite ? "i-material-symbols:bookmark-rounded" : "i-material-symbols:bookmark-add-outline-rounded", onclick: toggleFavorite - }, - {icon: "i-material-symbols:more-vert", onclick: () => alert('More clicked')} -]} /> + } +]}> + goto(`/import/netease?id=${meta.id}`)}>{t.updateFromNetease} +
From c5293f34b3e73d53e1caac6b65d6bcb4468d2b7b Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:36:14 +0800 Subject: [PATCH 5/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bac48ac..d22bf07 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Practice Japanese Karaoke lyrics reading and typing at the same time with KaraDa * [x] i18n * [x] 404 page * [x] Previous song / next song buttons -* [ ] Update an existing playlist +* [x] Update an existing playlist * [ ] Allow users to correct lyric pronunciations through correction feedback * [ ] Correct lyrics timing inconsistencies (i.e. 网易云的歌词因为是业余用户上传的,时间戳不一定准确。但是 waveform 里面可以分析出每句歌词的具体开始结束时间,也许可以自动修正)