Merge branch 'feature#404-page'
This commit is contained in:
@@ -30,9 +30,9 @@ Practice Japanese Karaoke lyrics reading and typing at the same time with KaraDa
|
|||||||
## Technical Tasks
|
## Technical Tasks
|
||||||
|
|
||||||
* [x] i18n
|
* [x] i18n
|
||||||
* [ ] 404 page
|
* [x] 404 page
|
||||||
* [ ] Previous song / next song buttons
|
* [x] Previous song / next song buttons
|
||||||
* [ ] Update an existing playlist
|
* [x] Update an existing playlist
|
||||||
* [ ] Allow users to correct lyric pronunciations through correction feedback
|
* [ ] Allow users to correct lyric pronunciations through correction feedback
|
||||||
* [ ] Correct lyrics timing inconsistencies (i.e. 网易云的歌词因为是业余用户上传的,时间戳不一定准确。但是 waveform 里面可以分析出每句歌词的具体开始结束时间,也许可以自动修正)
|
* [ ] Correct lyrics timing inconsistencies (i.e. 网易云的歌词因为是业余用户上传的,时间戳不一定准确。但是 waveform 里面可以分析出每句歌词的具体开始结束时间,也许可以自动修正)
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -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!',
|
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',
|
inputLabel: 'NetEase Playlist Link / ID',
|
||||||
btnStart: 'Start Import',
|
btnStart: 'Start Import',
|
||||||
|
btnUpdate: 'Update Playlist',
|
||||||
btnView: 'View Playlist'
|
btnView: 'View Playlist'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,7 +48,8 @@ export default {
|
|||||||
count: 'Songs: ',
|
count: 'Songs: ',
|
||||||
startPractice: 'Start Practice',
|
startPractice: 'Start Practice',
|
||||||
songList: 'Song List',
|
songList: 'Song List',
|
||||||
songs: 'songs'
|
songs: 'songs',
|
||||||
|
updateFromNetease: 'Update from NetEase'
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
mine: 'My Playlists',
|
mine: 'My Playlists',
|
||||||
|
|||||||
+3
-1
@@ -37,6 +37,7 @@ export default {
|
|||||||
tip: 'NetEase Musicアプリでお気に入りの日本語プレイリストを見つけ、共有をクリックし、リンクをコピーしてここに貼り付けると、インポートを開始できます!',
|
tip: 'NetEase Musicアプリでお気に入りの日本語プレイリストを見つけ、共有をクリックし、リンクをコピーしてここに貼り付けると、インポートを開始できます!',
|
||||||
inputLabel: 'NetEaseプレイリストリンク / ID',
|
inputLabel: 'NetEaseプレイリストリンク / ID',
|
||||||
btnStart: 'インポート開始',
|
btnStart: 'インポート開始',
|
||||||
|
btnUpdate: 'プレイリストを更新',
|
||||||
btnView: 'プレイリストを表示'
|
btnView: 'プレイリストを表示'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,7 +48,8 @@ export default {
|
|||||||
count: '曲数: ',
|
count: '曲数: ',
|
||||||
startPractice: '練習開始',
|
startPractice: '練習開始',
|
||||||
songList: '曲リスト',
|
songList: '曲リスト',
|
||||||
songs: '曲'
|
songs: '曲',
|
||||||
|
updateFromNetease: 'NetEaseから更新'
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
mine: 'マイプレイリスト',
|
mine: 'マイプレイリスト',
|
||||||
|
|||||||
+3
-1
@@ -37,6 +37,7 @@ export default {
|
|||||||
tip: '去网易云 APP 找一个你喜欢的日本语歌单,点击分享,再点击复制链接,然后把链接粘贴到这里就可以开始导入了!',
|
tip: '去网易云 APP 找一个你喜欢的日本语歌单,点击分享,再点击复制链接,然后把链接粘贴到这里就可以开始导入了!',
|
||||||
inputLabel: '网易云歌单链接 / ID',
|
inputLabel: '网易云歌单链接 / ID',
|
||||||
btnStart: '开始导入',
|
btnStart: '开始导入',
|
||||||
|
btnUpdate: '更新歌单',
|
||||||
btnView: '查看歌单'
|
btnView: '查看歌单'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,7 +48,8 @@ export default {
|
|||||||
count: '歌曲数: ',
|
count: '歌曲数: ',
|
||||||
startPractice: '开始练习',
|
startPractice: '开始练习',
|
||||||
songList: '歌曲列表',
|
songList: '歌曲列表',
|
||||||
songs: '首歌曲'
|
songs: '首歌曲',
|
||||||
|
updateFromNetease: '从网易云更新歌单'
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
mine: '我的歌单',
|
mine: '我的歌单',
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export const getSession = (id: string) => sessions.get(id)
|
|||||||
* @returns Import session
|
* @returns Import session
|
||||||
*/
|
*/
|
||||||
export async function startImport(link: string, userId?: number): Promise<ImportSession> {
|
export async function startImport(link: string, userId?: number): Promise<ImportSession> {
|
||||||
const meta = await getPlaylistRaw(parsePlaylistRef(link))
|
const meta = await getPlaylistRaw(parsePlaylistRef(link), true)
|
||||||
const importId = crypto.randomUUID()
|
const importId = crypto.randomUUID()
|
||||||
|
|
||||||
const session: ImportSession = {
|
const session: ImportSession = {
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
|
|
||||||
<LinearProgress percent={percentage ?? 0}/>
|
<LinearProgress percent={percentage ?? 0}/>
|
||||||
|
|
||||||
<div class="vbox p-content scroll-here gap-8px">
|
<div class="vbox p-content scroll-here gap-8px overflow-x-hidden">
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
<div class="hbox gap-12px items-center h-40px">
|
<div class="hbox gap-12px items-center h-40px">
|
||||||
<span class="{item.icon} text-xl"></span>
|
<span class="{item.icon} text-xl"></span>
|
||||||
<div class="vbox">
|
<div class="vbox min-w-0 flex-1">
|
||||||
<span class="m3-font-title-medium">{item.title}</span>
|
<span class="m3-font-title-medium truncate">{item.title}</span>
|
||||||
{#if item.subtitle}
|
{#if item.subtitle}
|
||||||
<span class="m3-font-body-small mfg-on-surface-variant">{item.subtitle}</span>
|
<span class="m3-font-body-small mfg-on-surface-variant truncate">{item.subtitle}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
import AppBar from "$lib/ui/appbar/AppBar.svelte"
|
import AppBar from "$lib/ui/appbar/AppBar.svelte"
|
||||||
import MenuItem from "$lib/ui/material3/MenuItem.svelte"
|
import MenuItem from "$lib/ui/material3/MenuItem.svelte"
|
||||||
import { artistAndAlbum } from "$lib/utils"
|
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 {
|
interface Props {
|
||||||
song: NeteaseSong
|
song: NeteaseSong
|
||||||
settings: TypingSettings
|
settings: TypingSettings
|
||||||
loc?: UserData['loc']
|
loc?: UserData['loc']
|
||||||
|
playlist?: NeteasePlaylist
|
||||||
showRomajiOnError?: boolean
|
showRomajiOnError?: boolean
|
||||||
disableHideRepeated?: boolean
|
disableHideRepeated?: boolean
|
||||||
isKaraoke?: boolean
|
isKaraoke?: boolean
|
||||||
@@ -17,12 +21,30 @@
|
|||||||
song,
|
song,
|
||||||
settings = $bindable(),
|
settings = $bindable(),
|
||||||
loc = $bindable(),
|
loc = $bindable(),
|
||||||
|
playlist,
|
||||||
showRomajiOnError = true,
|
showRomajiOnError = true,
|
||||||
disableHideRepeated = false,
|
disableHideRepeated = false,
|
||||||
isKaraoke = false
|
isKaraoke = false
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
let isHideRepeated = $derived(settings.hideRepeated && !disableHideRepeated)
|
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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppBar title={song.name} sub={artistAndAlbum(song)}>
|
<AppBar title={song.name} sub={artistAndAlbum(song)}>
|
||||||
@@ -40,7 +62,13 @@
|
|||||||
onclick={() => settings.hideRepeated = !settings.hideRepeated}>{isHideRepeated ? "显示重复行" : "隐藏重复行"}</MenuItem>
|
onclick={() => settings.hideRepeated = !settings.hideRepeated}>{isHideRepeated ? "显示重复行" : "隐藏重复行"}</MenuItem>
|
||||||
|
|
||||||
{#if loc}
|
{#if loc}
|
||||||
<MenuItem icon={loc.playMode === 'random' ? "i-material-symbols:shuffle-rounded" : "i-material-symbols:repeat-rounded"} onclick={() =>
|
<MenuItem icon={loc.playMode === 'random' ? "i-material-symbols:shuffle-rounded" : "i-material-symbols:repeat-rounded"}
|
||||||
loc.playMode = loc.playMode === 'random' ? 'sequential' : 'random'}>{loc.playMode === 'random' ? "当前:随机播放" : "当前:顺序播放"}</MenuItem>
|
onclick={() => loc!.playMode = loc!.playMode === 'random' ? 'sequential' : 'random'}>
|
||||||
|
{loc.playMode === 'random' ? "当前:随机播放" : "当前:顺序播放"}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{#if nextSongId}
|
||||||
|
<MenuItem icon="i-material-symbols:skip-next-rounded" onclick={handleNext}>下首</MenuItem>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { NeteasePlaylist, UserData } from "$lib/types"
|
||||||
|
|
||||||
|
export function getNextSong(playlist?: NeteasePlaylist, loc?: NonNullable<UserData['loc']>) {
|
||||||
|
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<UserData['loc']>, nextSongId: number): NonNullable<UserData['loc']> {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,12 @@
|
|||||||
import ErrorDialog from "$lib/ui/status/ErrorDialog.svelte"
|
import ErrorDialog from "$lib/ui/status/ErrorDialog.svelte"
|
||||||
import ProgressList from "$lib/ui/ProgressList.svelte"
|
import ProgressList from "$lib/ui/ProgressList.svelte"
|
||||||
import { getI18n } from "$lib/i18n"
|
import { getI18n } from "$lib/i18n"
|
||||||
|
import { page } from '$app/state'
|
||||||
|
|
||||||
const t = getI18n().import.netease
|
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 {
|
interface SongImportStatus {
|
||||||
song: NeteaseSong
|
song: NeteaseSong
|
||||||
@@ -91,7 +93,7 @@
|
|||||||
|
|
||||||
<div class="py-16px p-content">
|
<div class="py-16px p-content">
|
||||||
{#if status === 'idle'}
|
{#if status === 'idle'}
|
||||||
<Button big icon="i-material-symbols:download" onclick={startImport}>{t.btnStart}</Button>
|
<Button big icon={isUpdate ? "i-material-symbols:sync" : "i-material-symbols:download"} onclick={startImport}>{isUpdate ? t.btnUpdate : t.btnStart}</Button>
|
||||||
{:else if status === 'success'}
|
{:else if status === 'success'}
|
||||||
<a href="/playlist/{id}">
|
<a href="/playlist/{id}">
|
||||||
<Button big icon="i-material-symbols:right-arrow">{t.btnView}</Button>
|
<Button big icon="i-material-symbols:right-arrow">{t.btnView}</Button>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import SongInfo from "$lib/ui/listitem/SongInfo.svelte"
|
import SongInfo from "$lib/ui/listitem/SongInfo.svelte"
|
||||||
import { API } from "$lib/client"
|
import { API } from "$lib/client"
|
||||||
import { getI18n } from "$lib/i18n"
|
import { getI18n } from "$lib/i18n"
|
||||||
|
import MenuItem from "$lib/ui/material3/MenuItem.svelte";
|
||||||
|
|
||||||
const t = getI18n().playlist.detail
|
const t = getI18n().playlist.detail
|
||||||
|
|
||||||
@@ -49,9 +50,10 @@
|
|||||||
{
|
{
|
||||||
icon: isFavorite ? "i-material-symbols:bookmark-rounded" : "i-material-symbols:bookmark-add-outline-rounded",
|
icon: isFavorite ? "i-material-symbols:bookmark-rounded" : "i-material-symbols:bookmark-add-outline-rounded",
|
||||||
onclick: toggleFavorite
|
onclick: toggleFavorite
|
||||||
},
|
}
|
||||||
{icon: "i-material-symbols:more-vert", onclick: () => alert('More clicked')}
|
]}>
|
||||||
]} />
|
<MenuItem icon="i-material-symbols:update" onclick={() => goto(`/import/netease?id=${meta.id}`)}>{t.updateFromNetease}</MenuItem>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
<div class="hbox px-16px py-8px gap-24px">
|
<div class="hbox px-16px py-8px gap-24px">
|
||||||
<img src="{meta.coverImgUrl}" alt="" class="size-128px rounded-16px">
|
<img src="{meta.coverImgUrl}" alt="" class="size-128px rounded-16px">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Chart from "chart.js/auto"
|
import Chart from "chart.js/auto"
|
||||||
import { API } from "$lib/client"
|
import { API } from "$lib/client"
|
||||||
import { getI18n } from "$lib/i18n"
|
import { getI18n } from "$lib/i18n"
|
||||||
|
import { getNextSong, getNextLoc } from "$lib/ui/player/SongSwitching"
|
||||||
|
|
||||||
const t = getI18n().results
|
const t = getI18n().results
|
||||||
|
|
||||||
@@ -105,35 +106,15 @@
|
|||||||
|
|
||||||
// Compute next state immediately
|
// Compute next state immediately
|
||||||
if (playlist && loc && isCurrentResult) {
|
if (playlist && loc && isCurrentResult) {
|
||||||
if (loc.playMode === 'random') {
|
nextSongId = getNextSong(playlist, loc)
|
||||||
const unplayed = playlist.tracks.filter((t: NeteaseSong) => !loc.playedSongIds.includes(t.id))
|
if (nextSongId === null) isPlaylistFinished = true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleNext() {
|
async function handleNext() {
|
||||||
if (nextSongId !== null) {
|
if (nextSongId !== null) {
|
||||||
if (!data.user.data.loc || !data.playlist) return
|
if (!data.user.data.loc || !data.playlist) return
|
||||||
|
|
||||||
const nextIndex = data.playlist.tracks.findIndex((t: NeteaseSong) => t.id === nextSongId)
|
const newLoc = getNextLoc(data.playlist, data.user.data.loc, nextSongId)
|
||||||
|
|
||||||
const newLoc = {
|
|
||||||
...data.user.data.loc,
|
|
||||||
currentSongIndex: nextIndex,
|
|
||||||
isFinished: false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newLoc.playedSongIds.includes(nextSongId)) {
|
|
||||||
newLoc.playedSongIds = [...newLoc.playedSongIds, nextSongId]
|
|
||||||
}
|
|
||||||
|
|
||||||
data.user.data.loc = newLoc
|
data.user.data.loc = newLoc
|
||||||
await API.saveUserData({ loc: newLoc })
|
await API.saveUserData({ loc: newLoc })
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { getSongRaw } from "$lib/server/songs"
|
|
||||||
|
|
||||||
export const load = async ({ params }) => {
|
|
||||||
const song = await getSongRaw(+params.id)
|
|
||||||
return { song }
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,23 @@
|
|||||||
import { API } from "$lib/client"
|
import { API } from "$lib/client"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import Button from "$lib/ui/Button.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 ProgressList from "$lib/ui/ProgressList.svelte"
|
||||||
import { goto } from "$app/navigation"
|
import { goto } from "$app/navigation"
|
||||||
import { artistAndAlbum } from "$lib/utils"
|
import { artistAndAlbum } from "$lib/utils"
|
||||||
import { getI18n } from "$lib/i18n"
|
import { getI18n } from "$lib/i18n"
|
||||||
|
import { typingSettingsDefault } from "$lib/types"
|
||||||
|
|
||||||
const t = getI18n().song.mode
|
const t = getI18n().song.mode
|
||||||
|
|
||||||
let { data } = $props()
|
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({
|
let taskStatus = $state({
|
||||||
lyrics: false,
|
lyrics: false,
|
||||||
ai: 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 },
|
{ 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<any[]>([])
|
let progressItems = $state<any[]>([])
|
||||||
let progressPercentage = $state(0)
|
let progressPercentage = $state(0)
|
||||||
|
|
||||||
@@ -33,7 +40,6 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function startLoading() {
|
async function startLoading() {
|
||||||
loadStatus = "loading"
|
|
||||||
await API.song.prepare(data.song.id)
|
await API.song.prepare(data.song.id)
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
const res = await API.song.status(data.song.id)
|
const res = await API.song.status(data.song.id)
|
||||||
@@ -62,7 +68,6 @@
|
|||||||
|
|
||||||
if (state.status === "done") {
|
if (state.status === "done") {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
loadStatus = "done"
|
|
||||||
progressPercentage = 100
|
progressPercentage = 100
|
||||||
} else if (state.status === "error") {
|
} else if (state.status === "error") {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
@@ -71,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppBar title={data.song.name} sub={artistAndAlbum(data.song)} />
|
<PlayerAppBar song={data.song} bind:settings bind:loc playlist={data.playlist} />
|
||||||
|
|
||||||
<ProgressList percentage={progressPercentage} items={progressItems} />
|
<ProgressList percentage={progressPercentage} items={progressItems} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { PageServerLoad } from './$types'
|
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'
|
import { redirect } from '@sveltejs/kit'
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
const songId = +params.id
|
const songId = +params.id
|
||||||
const song = await getSongRaw(songId)
|
|
||||||
const hasLrc = await checkLyricsProcessed(songId)
|
const hasLrc = await checkLyricsProcessed(songId)
|
||||||
|
|
||||||
if (!hasLrc) throw redirect(302, `/song/${songId}`)
|
if (!hasLrc) throw redirect(302, `/song/${songId}`)
|
||||||
@@ -12,5 +11,5 @@ export const load: PageServerLoad = async ({ params }) => {
|
|||||||
const lrc = await getLyricsProcessed(songId)!
|
const lrc = await getLyricsProcessed(songId)!
|
||||||
const audioData = await getSongUrl(songId)
|
const audioData = await getSongUrl(songId)
|
||||||
|
|
||||||
return { song, lrc, audioData }
|
return { lrc, audioData }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
let settings = $state(data.user.data?.typingSettings ?? typingSettingsDefault)
|
let settings = $state(data.user.data?.typingSettings ?? typingSettingsDefault)
|
||||||
$effect(() => { API.saveUserData({ typingSettings: settings }) })
|
$effect(() => { API.saveUserData({ typingSettings: settings }) })
|
||||||
|
|
||||||
|
let loc = $state(data.user.data.loc)
|
||||||
|
$effect(() => { API.saveUserData({ loc }) })
|
||||||
|
|
||||||
let vocalsVolume = $state(100) // 0-100
|
let vocalsVolume = $state(100) // 0-100
|
||||||
|
|
||||||
// Process lyrics
|
// Process lyrics
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
|
|
||||||
<svelte:window onclick={() => musicControl?.ready()} onkeydown={() => musicControl?.ready()}/>
|
<svelte:window onclick={() => musicControl?.ready()} onkeydown={() => musicControl?.ready()}/>
|
||||||
|
|
||||||
<PlayerAppBar song={data.song} bind:settings showRomajiOnError={false} isKaraoke={true} disableHideRepeated />
|
<PlayerAppBar song={data.song} bind:settings bind:loc showRomajiOnError={false} isKaraoke={true} disableHideRepeated playlist={data.playlist} />
|
||||||
|
|
||||||
<div class="vbox p-content py-4 gap-2 mfg-on-surface-variant">
|
<div class="vbox p-content py-4 gap-2 mfg-on-surface-variant">
|
||||||
{#if data.audioData.vocalsUrl}
|
{#if data.audioData.vocalsUrl}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import type { PageServerLoad } from './$types'
|
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'
|
import { redirect } from '@sveltejs/kit'
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, url }) => {
|
export const load: PageServerLoad = async ({ params, url }) => {
|
||||||
const songId = +params.id
|
const songId = +params.id
|
||||||
const song = await getSongRaw(songId)
|
|
||||||
const hasLrc = await checkLyricsProcessed(songId)
|
const hasLrc = await checkLyricsProcessed(songId)
|
||||||
|
|
||||||
if (!hasLrc) throw redirect(302, `/song/${songId}`)
|
if (!hasLrc) throw redirect(302, `/song/${songId}`)
|
||||||
|
|
||||||
const lrc = await getLyricsProcessed(songId)!
|
const lrc = await getLyricsProcessed(songId)!
|
||||||
const audioUrl = url.searchParams.get('music') === 'true' ? await getSongUrl(songId) : undefined
|
const audioUrl = url.searchParams.get('music') === 'true' ? await getSongUrl(songId) : undefined
|
||||||
return { song, lrc, audioUrl }
|
return { lrc, audioUrl }
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
|
|
||||||
<svelte:window onclick={() => musicControl?.ready()} onkeydown={() => musicControl?.ready()}/>
|
<svelte:window onclick={() => musicControl?.ready()} onkeydown={() => musicControl?.ready()}/>
|
||||||
|
|
||||||
<PlayerAppBar song={data.song} bind:settings bind:loc disableHideRepeated={!!data.audioUrl} />
|
<PlayerAppBar song={data.song} bind:settings bind:loc disableHideRepeated={!!data.audioUrl} playlist={data.playlist} />
|
||||||
|
|
||||||
<LinearProgress percent={progress} />
|
<LinearProgress percent={progress} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user