[+] Next song button

This commit is contained in:
2025-11-23 10:09:37 +08:00
parent f70fd90032
commit f6ba9a8897
10 changed files with 93 additions and 45 deletions
+31 -3
View File
@@ -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}`)
}
}
</script>
<AppBar title={song.name} sub={artistAndAlbum(song)}>
@@ -40,7 +62,13 @@
onclick={() => settings.hideRepeated = !settings.hideRepeated}>{isHideRepeated ? "显示重复行" : "隐藏重复行"}</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>
<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>
{#if nextSongId}
<MenuItem icon="i-material-symbols:skip-next-rounded" onclick={handleNext}>下首</MenuItem>
{/if}
{/if}
</AppBar>
+29
View File
@@ -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]
}
}
+4 -23
View File
@@ -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 })
+10
View File
@@ -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 }
}
-6
View File
@@ -1,6 +0,0 @@
import { getSongRaw } from "$lib/server/songs"
export const load = async ({ params }) => {
const song = await getSongRaw(+params.id)
return { song }
}
+10 -5
View File
@@ -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<any[]>([])
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 @@
}
</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} />
+2 -3
View File
@@ -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 }
}
+4 -1
View File
@@ -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 @@
<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">
{#if data.audioData.vocalsUrl}
+2 -3
View File
@@ -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 }
}
+1 -1
View File
@@ -157,7 +157,7 @@
<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} />