diff --git a/package.json b/package.json index 46e006e32..893cb41ff 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.1", "@intrnl/xxhash64": "^0.1.2", - "@song-spotlight/api": "^2.1.3", + "@song-spotlight/api": "^2.2.0", "@streamparser/json": "^0.0.22", "@types/less": "^3.0.8", "@types/stylus": "^0.48.42", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc0d40c38..fa8bb4848 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^0.1.2 version: 0.1.2 '@song-spotlight/api': - specifier: ^2.1.3 - version: 2.1.3(typescript@6.0.3) + specifier: ^2.2.0 + version: 2.2.0(typescript@6.0.3) '@streamparser/json': specifier: ^0.0.22 version: 0.0.22 @@ -575,8 +575,8 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@song-spotlight/api@2.1.3': - resolution: {integrity: sha512-Wai7JHd9lrHtM/lkhfWQIFjIWKtP8iD+zj4Lnwbs/ykELR0MB2k56TDnQaRAelvu8XOygt27wfDBDQSEepPqdA==} + '@song-spotlight/api@2.2.0': + resolution: {integrity: sha512-zKH9X+G1uRYmEHFgluExPEFJWtA87PG8C39Xng0XG7Oh42ycOHjZtqtyEbfGM+hs+QEVos4wOoySeBAggnK7Iw==} peerDependencies: typescript: ^5 @@ -2698,7 +2698,7 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@song-spotlight/api@2.1.3(typescript@6.0.3)': + '@song-spotlight/api@2.2.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 optionalDependencies: @@ -2958,10 +2958,10 @@ snapshots: array-includes@3.1.9: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 is-string: 1.1.1 @@ -2978,10 +2978,10 @@ snapshots: array.prototype.findlastindex@1.2.6: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 @@ -2995,9 +2995,9 @@ snapshots: array.prototype.flat@1.3.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: @@ -4298,9 +4298,9 @@ snapshots: object.groupby@1.0.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 object.values@1.2.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0b52f44eb..c6cfde866 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -17,3 +17,5 @@ allowBuilds: esbuild: true pmOnFail: warn verifyDepsBeforeRun: prompt +minimumReleaseAgeExclude: + - '@song-spotlight/api@2.2.0' diff --git a/src/equicordplugins/songSpotlight.desktop/index.tsx b/src/equicordplugins/songSpotlight.desktop/index.tsx index 8e9f99f40..c0e9f2fbb 100644 --- a/src/equicordplugins/songSpotlight.desktop/index.tsx +++ b/src/equicordplugins/songSpotlight.desktop/index.tsx @@ -12,7 +12,6 @@ import definePlugin from "@utils/types"; import { useAuthorizationStore } from "./lib/stores/AuthorizationStore"; import { useSongStore } from "./lib/stores/SongStore"; -import { Native } from "./service"; import settings from "./settings"; import ProfileSongs from "./ui/songs/ProfileSongs"; import WidgetSongs from "./ui/songs/WidgetSongs"; @@ -48,12 +47,6 @@ export default definePlugin({ }, }, start() { - // the cache lives in native.ts so it persists between reloads and - // only gets cleared on full restart. we don't want that since - // audio preview URLs expire very fast, so we just clear it on - // plugin restart instead - Native.clearCache(); - useSongStore.getState().$refresh(); useAuthorizationStore.persist.rehydrate(); }, diff --git a/src/equicordplugins/songSpotlight.desktop/native.ts b/src/equicordplugins/songSpotlight.desktop/native.ts index ea8fc1929..6092795c5 100644 --- a/src/equicordplugins/songSpotlight.desktop/native.ts +++ b/src/equicordplugins/songSpotlight.desktop/native.ts @@ -20,7 +20,3 @@ export async function renderSong(_: IpcMainInvokeEvent, song: Song) { export async function validateSong(_: IpcMainInvokeEvent, song: Song) { return handlers.validateSong(song); } - -export function clearCache() { - return handlers.clearCache(); -} diff --git a/src/equicordplugins/songSpotlight.desktop/settings.tsx b/src/equicordplugins/songSpotlight.desktop/settings.tsx index c374c6612..b9d554723 100644 --- a/src/equicordplugins/songSpotlight.desktop/settings.tsx +++ b/src/equicordplugins/songSpotlight.desktop/settings.tsx @@ -22,6 +22,13 @@ export default definePluginSettings({ default: apiConstants.songLimit, markers: makeRange(1, 3), }, + previewVolume: { + type: OptionType.SLIDER, + description: "Volume of song previews when played", + markers: [0, 25, 50, 100], + default: 100, + stickToMarkers: false + }, manager: { type: OptionType.COMPONENT, component: () => , diff --git a/src/equicordplugins/songSpotlight.desktop/style.css b/src/equicordplugins/songSpotlight.desktop/style.css index bb44f9aaa..8897eb8b8 100644 --- a/src/equicordplugins/songSpotlight.desktop/style.css +++ b/src/equicordplugins/songSpotlight.desktop/style.css @@ -145,6 +145,7 @@ gap: 6px; &.vc-songspotlight-songs-container-sidebar { + margin: 12px 16px 0; padding: 12px; } } @@ -201,10 +202,6 @@ padding: 8px; } -.user-profile-popout:not(.custom-user-profile-theme) .vc-songspotlight-song-container { - background: var(--opacity-16); -} - .vc-songspotlight-song { height: 100%; animation: songspotlight-fade-in var(--time) linear forwards; @@ -301,7 +298,7 @@ .vc-songspotlight-song-progress { position: absolute; inset: 0; - color: white; + color: var(--icon-default); pointer-events: none; > circle { @@ -369,4 +366,4 @@ from { opacity: 0; } -} +} \ No newline at end of file diff --git a/src/equicordplugins/songSpotlight.desktop/ui/components/AudioPlayer.tsx b/src/equicordplugins/songSpotlight.desktop/ui/components/AudioPlayer.tsx index 144efb75f..904625837 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/components/AudioPlayer.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/components/AudioPlayer.tsx @@ -5,109 +5,138 @@ */ import { logger } from "@equicordplugins/songSpotlight.desktop/lib/utils"; -import { RenderInfoEntry } from "@song-spotlight/api/handlers"; +import settings from "@equicordplugins/songSpotlight.desktop/settings"; +import { RenderInfoEntry, RenderInfoEntryAudio } from "@song-spotlight/api/handlers"; import { showToast, Toasts, useCallback, useEffect, useMemo, useRef } from "@webpack/common"; import { RefObject } from "react"; +interface AudioItemProps { + audio: RenderInfoEntryAudio; + index: number; + handleRef(index: number, node: HTMLAudioElement | null): void; + handleLoaded(index: number, state: boolean): void; + handleStopped(index: number, ended?: boolean): void; +} + +function AudioItem({ audio, index, handleRef, handleLoaded, handleStopped }: AudioItemProps) { + const refCallback = useCallback((node: HTMLAudioElement | null) => handleRef(index, node), [index, handleRef]); + const loadedCallback = useCallback(() => handleLoaded(index, true), [index, handleLoaded]); + const erroredCallback = useCallback(() => handleLoaded(index, false), [index, handleLoaded]); + const pausedCallback = useCallback(() => handleStopped(index), [index, handleStopped]); + const endedCallback = useCallback(() => handleStopped(index, true), [index, handleStopped]); + + return ; +} + interface AudioPlayerProps { audioRef: RefObject; list: RenderInfoEntry[]; - playing: number | false; - setPlaying(playing: number | false): void; - setLoaded(index: number, state: boolean): void; + playing?: number; + setPlaying(state: number | undefined): void; + setLoadedAudio(index: number, state: boolean): void; } -const DEFAULT_VOLUME = 0.35; +const BASE_VOLUME = 0.35; -// only allow one song to play at a time +// only allows one song to play at a time let globalPlaying: HTMLAudioElement | undefined = undefined; -export default function AudioPlayer({ audioRef, list, playing, setPlaying, setLoaded }: AudioPlayerProps) { - const urls = useMemo(() => list.map(x => x.audio?.previewUrl), [list]); - const audios = useRef(new Map()); +export default function AudioPlayer({ audioRef, list, playing, setPlaying, setLoadedAudio }: AudioPlayerProps) { + const audios = useMemo(() => list.map(x => x.audio), [list]); + const nodes = useRef(new Map()); const loaded = useRef(new Set()); + const nodeEvents = useRef(new Map void>()); + const handleRef = useCallback((index: number, node: HTMLAudioElement | null) => { + if (node) { + nodes.current.set(index, node); + + const audio = audios[index]; + if (audio?.previewStart === undefined || !audio.previewSlice) return; + + const endTime = (audio.previewStart + audio.previewSlice) / 1000; + function timeUpdated() { + if (node && node.currentTime >= endTime) { + node.volume = 0; + handleStopped(index, true); + } + } + + node.addEventListener("timeupdate", timeUpdated); + nodeEvents.current.set(index, () => node.removeEventListener("timeupdate", timeUpdated)); + } else { + nodes.current.delete(index); + nodeEvents.current.get(index)?.(); + nodeEvents.current.delete(index); + } + }, [audios]); + + const handleLoaded = useCallback((index: number, state: boolean) => { + if (state) loaded.current.add(index); + else loaded.current.delete(index); + setLoadedAudio(index, state); + }, [setLoadedAudio]); + + const handleStopped = useCallback((index: number, ended?: boolean) => { + if (ended) { + const nextIndex = loaded.current.values().toArray().sort().find(x => x > index); + setPlaying(nextIndex !== -1 ? nextIndex : undefined); + } else if (playing === index) { + setPlaying(undefined); + } + }, [playing, setPlaying]); + useEffect(() => { - if (playing !== false) { - const audio = audios.current.get(playing); - if (audio && loaded.current.has(playing)) { + if (playing !== undefined) { + const audio = audios[playing], node = nodes.current.get(playing); + if (audio && node && loaded.current.has(playing)) { if (globalPlaying) globalPlaying.pause(); - audio.currentTime = 0; - audio.volume = DEFAULT_VOLUME; - audio.play().catch(error => { + node.currentTime = audio.previewStart ? audio.previewStart / 1000 : 0; + node.volume = BASE_VOLUME * (settings.store.previewVolume / 100); + node.play().catch(error => { showToast("Failed to play song preview!", Toasts.Type.FAILURE); logger.error("Failed to play audio", error); - setPlaying(false); + setPlaying(undefined); }); - globalPlaying = audio; + globalPlaying = node; } else { - setPlaying(false); - } - } else { - for (const audio of audios.current.values()) { - if (globalPlaying === audio) audio.pause(); + setPlaying(undefined); } } - for (const audio of audios.current.values()) { - if (audio !== globalPlaying && !audio.paused) { - audio.pause(); - } else if (audio === globalPlaying && audio.paused) { + for (const [index, node] of nodes.current) { + if (playing !== index && !node.paused) { + node.pause(); + } else if (globalPlaying === node && node.paused) { globalPlaying = undefined; audioRef.current = undefined; - } else if (audio === globalPlaying) { - audioRef.current = audio; + } else if (node === globalPlaying) { + audioRef.current = node; } } - }, [playing]); - - const handleRef = useCallback((index: number, audio: HTMLAudioElement | null) => { - if (audio) audios.current.set(index, audio); - else audios.current.delete(index); - }, []); - - const handleLoaded = useCallback((index: number) => { - loaded.current.add(index); - setLoaded(index, true); - }, [setLoaded]); - - const handleErrored = useCallback((index: number) => { - loaded.current.delete(index); - setLoaded(index, false); - }, [setLoaded]); - - // onPaused runs before onEnded - const justPaused = useRef(undefined); - - const handleEnded = useCallback((index: number) => { - if (justPaused.current !== index && playing !== index) return; - - const nextIndex = urls.findIndex((url, j) => url && j > index); - setPlaying(nextIndex !== -1 ? nextIndex : false); - }, [playing, setPlaying]); - - const handlePaused = useCallback((index: number) => { - if (playing === index) { - justPaused.current = index; - setPlaying(false); - } - }, [playing, setPlaying]); + }, [playing, audios]); return ( - {urls.map((url, i) => - url && ( - handleRef(i, audio)} - onLoadedData={() => handleLoaded(i)} - onError={() => handleErrored(i)} - onEnded={() => handleEnded(i)} - onPause={() => handlePaused(i)} + {audios.map((audio, index) => + audio && ( + ) )} diff --git a/src/equicordplugins/songSpotlight.desktop/ui/components/ProgressCircle.tsx b/src/equicordplugins/songSpotlight.desktop/ui/components/ProgressCircle.tsx index 26654a288..2bd8178b5 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/components/ProgressCircle.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/components/ProgressCircle.tsx @@ -4,18 +4,22 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { RenderInfoEntry } from "@song-spotlight/api/handlers"; import { useEffect, useMemo, useState } from "@webpack/common"; import { JSX, RefObject } from "react"; interface ProgressCircleProps extends SvgProps { border: number; audioRef: RefObject; + playingRef: RefObject; } type SvgProps = JSX.IntrinsicElements["svg"]; -export default function ProgressCircle({ border, audioRef, ...props }: ProgressCircleProps) { +const SIZE = 50; + +export default function ProgressCircle({ border, audioRef, playingRef, ...props }: ProgressCircleProps) { const { radius, stroke, circumference } = useMemo(() => { - const radius = 50 - border * 2; + const radius = SIZE - border * 2; return { radius, stroke: border * 2, @@ -26,9 +30,14 @@ export default function ProgressCircle({ border, audioRef, ...props }: ProgressC useEffect(() => { let handle = requestAnimationFrame(function update() { - const audio = audioRef.current; - if (audio && !Number.isNaN(audio.duration) && !audio.paused) { - setProgress(audio.currentTime / audio.duration); + const audio = audioRef.current, playing = playingRef.current?.audio; + if (audio && playing && !Number.isNaN(audio.duration) && !audio.paused) { + let start = 0, slice = audio.duration; + if (playing.previewStart !== undefined && playing.previewSlice) { + start = playing.previewStart / 1000; + slice = playing.previewSlice / 1000; + } + setProgress(Math.min(Math.max((audio.currentTime - start) / slice, 0), 1)); } else { setProgress(0); } @@ -42,11 +51,11 @@ export default function ProgressCircle({ border, audioRef, ...props }: ProgressC return ( diff --git a/src/equicordplugins/songSpotlight.desktop/ui/settings/SongList.tsx b/src/equicordplugins/songSpotlight.desktop/ui/settings/SongList.tsx index f09bdae34..4bf8b47a1 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/settings/SongList.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/settings/SongList.tsx @@ -31,18 +31,29 @@ import { import { DragEvent } from "react"; interface EditableSongProps { + index: number; song: Song; insert?: "top" | "bottom"; - setSongRef(div: HTMLAnchorElement | null): void; - onDrag(event: DragEvent): void; + setSongRef(index: number, div: HTMLAnchorElement | null): void; + onDrag(index: number, event: DragEvent): void; onDrop(song: Song): void; onRemove(song: Song): void; } -function EditableSong({ song, insert, setSongRef, onDrag, onDrop, onRemove }: EditableSongProps) { +function EditableSong({ index, song, insert, setSongRef, onDrag, onDrop, onRemove }: EditableSongProps) { const { render, failed } = useRender(song); const [dragging, setDragging] = useState(false); + const refCallback = useCallback((div: HTMLAnchorElement | null) => setSongRef(index, div), [index, setSongRef]); + const dragCallback = useCallback((event: DragEvent) => { + setDragging(true); + onDrag(index, event); + }, [index, onDrag]); + const dragEndCallback = useCallback(() => { + setDragging(false); + onDrop(song); + }, [song, onDrop]); + return ( { - setDragging(true); - onDrag(event); - }} - onDrag={onDrag} - onDragEnd={() => { - setDragging(false); - onDrop(song); - }} - useDefaultUnderlineStyles={false} - ref={div => setSongRef(div)} + onDragStart={dragCallback} + onDrag={dragCallback} + onDragEnd={dragEndCallback} + ref={refCallback} className={cl("editable-song-container")} > @@ -211,10 +215,11 @@ export default function SongList({ localData, setLocalData }: SongListProps) { if (slot === "song") { return ( handleRef(i, element)} - onDrag={event => handleDrag(i, event)} + setSongRef={handleRef} + onDrag={handleDrag} onDrop={handleDrop} onRemove={handleRemove} key={sid(song)} diff --git a/src/equicordplugins/songSpotlight.desktop/ui/songs/CollapsedProfileSongs.tsx b/src/equicordplugins/songSpotlight.desktop/ui/songs/CollapsedProfileSongs.tsx index 61909fea3..4671070d4 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/songs/CollapsedProfileSongs.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/songs/CollapsedProfileSongs.tsx @@ -17,6 +17,7 @@ import { RenderSongInfo } from "@song-spotlight/api/handlers"; import { UserData } from "@song-spotlight/api/structs"; import { sid } from "@song-spotlight/api/util"; import { classes } from "@utils/index"; +import { User } from "@vencord/discord-types"; import { SelectedChannelStore, SelectedGuildStore, @@ -27,12 +28,12 @@ import { useState, } from "@webpack/common"; -import { ProfileSongsProps } from "./ProfileSongs"; - const shownSongs = 4; -interface CollapsedProfileSongsProps extends ProfileSongsProps { +interface CollapsedProfileSongsProps { data?: UserData; + user: User; + isSideBar: boolean; } export default function CollapsedProfileSongs({ data, user, isSideBar }: CollapsedProfileSongsProps) { diff --git a/src/equicordplugins/songSpotlight.desktop/ui/songs/ProfileSongs.tsx b/src/equicordplugins/songSpotlight.desktop/ui/songs/ProfileSongs.tsx index 3f8428c3f..9753f3651 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/songs/ProfileSongs.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/songs/ProfileSongs.tsx @@ -39,7 +39,7 @@ import { import Song from "."; import CollapsedProfileSongs from "./CollapsedProfileSongs"; -export interface ProfileSongsProps { +interface ProfileSongsProps { user: User; isSideBar: boolean; } @@ -49,7 +49,7 @@ export default function ProfileSongs({ user, isSideBar }: ProfileSongsProps) { const { isAuthorized } = useAuthorizationStore(); const { users } = useSongStore(); const { profileSongsLimit, collapseSongList } = settings.use(); - const userId = user?.id; + const userId = user.id; const data = users[userId]?.data; useEffect(() => { if (isAuthorized() && !data) listData(userId).catch(() => setFailed(true)); diff --git a/src/equicordplugins/songSpotlight.desktop/ui/songs/WidgetSongs.tsx b/src/equicordplugins/songSpotlight.desktop/ui/songs/WidgetSongs.tsx index 6b802c1f5..26c613886 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/songs/WidgetSongs.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/songs/WidgetSongs.tsx @@ -21,7 +21,7 @@ import { JSX } from "react"; import Song from "."; -export interface WidgetSongsProps { +interface WidgetSongsProps { user: User; } diff --git a/src/equicordplugins/songSpotlight.desktop/ui/songs/index.tsx b/src/equicordplugins/songSpotlight.desktop/ui/songs/index.tsx index a1fd51434..afa8ce15d 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/songs/index.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/songs/index.tsx @@ -25,7 +25,7 @@ import AudioPlayer from "@equicordplugins/songSpotlight.desktop/ui/components/Au import ProgressCircle from "@equicordplugins/songSpotlight.desktop/ui/components/ProgressCircle"; import ServiceIcon from "@equicordplugins/songSpotlight.desktop/ui/components/ServiceIcon"; import { openSettingsModal } from "@equicordplugins/songSpotlight.desktop/ui/settings"; -import { RenderInfoEntryBased, RenderSongInfo } from "@song-spotlight/api/handlers"; +import { RenderInfoEntry, RenderInfoEntryBased, RenderSongInfo } from "@song-spotlight/api/handlers"; import { Song as SongType } from "@song-spotlight/api/structs"; import { isListLayout, sid } from "@song-spotlight/api/util"; import { copyWithToast } from "@utils/discord"; @@ -43,7 +43,7 @@ import { useCallback, useMemo, useRef, - useState, + useState } from "@webpack/common"; interface SongEntryProps { @@ -134,28 +134,29 @@ interface SongInfoProps { } function SongInfo({ owned, song, render, big }: SongInfoProps) { - const [playing, setPlaying] = useState(false); + const [playing, setPlaying] = useState(undefined); const [loaded, setLoaded] = useState(new Set()); + const setLoadedAudio = useCallback((index: number, state: boolean) => { + if (state) loaded.add(index); + else loaded.delete(index); + setLoaded(new Set(loaded)); + }, [loaded]); + const audios = useMemo(() => render.form === "single" ? [render.single] : render.list, [render]); const audioRef = useRef(undefined); + const playingRef = useRef(undefined); + playingRef.current = playing !== undefined ? audios[playing] : undefined; + const duration = useMemo( - () => { - if (playing !== false) { - return audios[playing].audio?.duration; - } else { - return render.form === "single" ? render.single.audio?.duration : undefined; - } - }, + () => + render.form === "single" ? + render.single.audio?.duration : + playing !== undefined ? + render.list[playing].audio?.duration : + undefined, [playing, render], ); - const setLoadedAudio = useCallback((index: number, state: boolean) => - setLoaded(ld => { - if (state) ld.add(index); - else ld.delete(index); - return new Set(ld); - }), []); - const baseSize = big ? "md" : "sm"; const subSize = big ? "sm" : "xs"; @@ -166,7 +167,7 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) { list={audios} playing={playing} setPlaying={setPlaying} - setLoaded={setLoadedAudio} + setLoadedAudio={setLoadedAudio} /> @@ -273,7 +274,7 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) { alignItems="flex-end" gap="6px" className={cl("song-player")} - data-idle={playing === false && !big} + data-idle={playing === undefined && !big} > {duration && ( @@ -282,19 +283,19 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) { )} { - const loadedI = loaded.values().toArray().sort()[0]; - if (loadedI === undefined) return; + if (playing !== undefined) return setPlaying(undefined); - if (playing !== false) setPlaying(false); - else setPlaying(loadedI); + const loadedIndex = loaded.values().toArray().sort()[0]; + if (loadedIndex !== undefined) setPlaying(loadedIndex); }} /> @@ -312,10 +313,8 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) { isLoaded={loaded.has(i)} isPlaying={playing === i} onClick={() => { - if (!loaded.has(i)) return; - - if (playing !== i) setPlaying(i); - else setPlaying(false); + if (playing === i) setPlaying(undefined); + else if (loaded.has(i)) setPlaying(i); }} big={big} key={entry.link}