SongSpotlight: shabloink (#1230)

* SongSpotlight: shabloink

* SongSpotlight: zazabloink

* Update AudioPlayer.tsx

* Update pnpm-workspace.yaml

* SongSpotlight: big refactors awoooooga

* [autofix.ci] apply automated fixes

* SongSpotlight: parity fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
nexpid 2026-07-09 22:34:54 +02:00 • committed by GitHub
parent cf08239295
commit 8e9d78c3aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 201 additions and 163 deletions

View file

@ -45,7 +45,7 @@
"@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1", "@ffmpeg/util": "^0.12.1",
"@intrnl/xxhash64": "^0.1.2", "@intrnl/xxhash64": "^0.1.2",
"@song-spotlight/api": "^2.1.3", "@song-spotlight/api": "^2.2.0",
"@streamparser/json": "^0.0.22", "@streamparser/json": "^0.0.22",
"@types/less": "^3.0.8", "@types/less": "^3.0.8",
"@types/stylus": "^0.48.42", "@types/stylus": "^0.48.42",

26
pnpm-lock.yaml generated
View file

@ -21,8 +21,8 @@ importers:
specifier: ^0.1.2 specifier: ^0.1.2
version: 0.1.2 version: 0.1.2
'@song-spotlight/api': '@song-spotlight/api':
specifier: ^2.1.3 specifier: ^2.2.0
version: 2.1.3(typescript@6.0.3) version: 2.2.0(typescript@6.0.3)
'@streamparser/json': '@streamparser/json':
specifier: ^0.0.22 specifier: ^0.0.22
version: 0.0.22 version: 0.0.22
@ -575,8 +575,8 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@song-spotlight/api@2.1.3': '@song-spotlight/api@2.2.0':
resolution: {integrity: sha512-Wai7JHd9lrHtM/lkhfWQIFjIWKtP8iD+zj4Lnwbs/ykELR0MB2k56TDnQaRAelvu8XOygt27wfDBDQSEepPqdA==} resolution: {integrity: sha512-zKH9X+G1uRYmEHFgluExPEFJWtA87PG8C39Xng0XG7Oh42ycOHjZtqtyEbfGM+hs+QEVos4wOoySeBAggnK7Iw==}
peerDependencies: peerDependencies:
typescript: ^5 typescript: ^5
@ -2698,7 +2698,7 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {} '@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: dependencies:
typescript: 6.0.3 typescript: 6.0.3
optionalDependencies: optionalDependencies:
@ -2958,10 +2958,10 @@ snapshots:
array-includes@3.1.9: array-includes@3.1.9:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.9
call-bound: 1.0.4 call-bound: 1.0.4
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.1 es-abstract: 1.24.2
es-object-atoms: 1.1.2 es-object-atoms: 1.1.2
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
is-string: 1.1.1 is-string: 1.1.1
@ -2978,10 +2978,10 @@ snapshots:
array.prototype.findlastindex@1.2.6: array.prototype.findlastindex@1.2.6:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.9
call-bound: 1.0.4 call-bound: 1.0.4
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.1 es-abstract: 1.24.2
es-errors: 1.3.0 es-errors: 1.3.0
es-object-atoms: 1.1.2 es-object-atoms: 1.1.2
es-shim-unscopables: 1.1.0 es-shim-unscopables: 1.1.0
@ -2995,9 +2995,9 @@ snapshots:
array.prototype.flat@1.3.3: array.prototype.flat@1.3.3:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.9
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.1 es-abstract: 1.24.2
es-shim-unscopables: 1.1.0 es-shim-unscopables: 1.1.0
array.prototype.flatmap@1.3.3: array.prototype.flatmap@1.3.3:
@ -4298,9 +4298,9 @@ snapshots:
object.groupby@1.0.3: object.groupby@1.0.3:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.9
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.24.1 es-abstract: 1.24.2
object.values@1.2.1: object.values@1.2.1:
dependencies: dependencies:

View file

@ -17,3 +17,5 @@ allowBuilds:
esbuild: true esbuild: true
pmOnFail: warn pmOnFail: warn
verifyDepsBeforeRun: prompt verifyDepsBeforeRun: prompt
minimumReleaseAgeExclude:
- '@song-spotlight/api@2.2.0'

View file

@ -12,7 +12,6 @@ import definePlugin from "@utils/types";
import { useAuthorizationStore } from "./lib/stores/AuthorizationStore"; import { useAuthorizationStore } from "./lib/stores/AuthorizationStore";
import { useSongStore } from "./lib/stores/SongStore"; import { useSongStore } from "./lib/stores/SongStore";
import { Native } from "./service";
import settings from "./settings"; import settings from "./settings";
import ProfileSongs from "./ui/songs/ProfileSongs"; import ProfileSongs from "./ui/songs/ProfileSongs";
import WidgetSongs from "./ui/songs/WidgetSongs"; import WidgetSongs from "./ui/songs/WidgetSongs";
@ -48,12 +47,6 @@ export default definePlugin({
}, },
}, },
start() { 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(); useSongStore.getState().$refresh();
useAuthorizationStore.persist.rehydrate(); useAuthorizationStore.persist.rehydrate();
}, },

View file

@ -20,7 +20,3 @@ export async function renderSong(_: IpcMainInvokeEvent, song: Song) {
export async function validateSong(_: IpcMainInvokeEvent, song: Song) { export async function validateSong(_: IpcMainInvokeEvent, song: Song) {
return handlers.validateSong(song); return handlers.validateSong(song);
} }
export function clearCache() {
return handlers.clearCache();
}

View file

@ -22,6 +22,13 @@ export default definePluginSettings({
default: apiConstants.songLimit, default: apiConstants.songLimit,
markers: makeRange(1, 3), 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: { manager: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
component: () => <Settings />, component: () => <Settings />,

View file

@ -145,6 +145,7 @@
gap: 6px; gap: 6px;
&.vc-songspotlight-songs-container-sidebar { &.vc-songspotlight-songs-container-sidebar {
margin: 12px 16px 0;
padding: 12px; padding: 12px;
} }
} }
@ -201,10 +202,6 @@
padding: 8px; padding: 8px;
} }
.user-profile-popout:not(.custom-user-profile-theme) .vc-songspotlight-song-container {
background: var(--opacity-16);
}
.vc-songspotlight-song { .vc-songspotlight-song {
height: 100%; height: 100%;
animation: songspotlight-fade-in var(--time) linear forwards; animation: songspotlight-fade-in var(--time) linear forwards;
@ -301,7 +298,7 @@
.vc-songspotlight-song-progress { .vc-songspotlight-song-progress {
position: absolute; position: absolute;
inset: 0; inset: 0;
color: white; color: var(--icon-default);
pointer-events: none; pointer-events: none;
> circle { > circle {
@ -369,4 +366,4 @@
from { from {
opacity: 0; opacity: 0;
} }
} }

View file

@ -5,109 +5,138 @@
*/ */
import { logger } from "@equicordplugins/songSpotlight.desktop/lib/utils"; 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 { showToast, Toasts, useCallback, useEffect, useMemo, useRef } from "@webpack/common";
import { RefObject } from "react"; 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 <audio
src={audio.previewUrl}
preload="metadata"
ref={refCallback}
onLoadedData={loadedCallback}
onError={erroredCallback}
onPause={pausedCallback}
onEnded={endedCallback}
/>;
}
interface AudioPlayerProps { interface AudioPlayerProps {
audioRef: RefObject<HTMLAudioElement | undefined>; audioRef: RefObject<HTMLAudioElement | undefined>;
list: RenderInfoEntry[]; list: RenderInfoEntry[];
playing: number | false; playing?: number;
setPlaying(playing: number | false): void; setPlaying(state: number | undefined): void;
setLoaded(index: number, state: boolean): 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; let globalPlaying: HTMLAudioElement | undefined = undefined;
export default function AudioPlayer({ audioRef, list, playing, setPlaying, setLoaded }: AudioPlayerProps) { export default function AudioPlayer({ audioRef, list, playing, setPlaying, setLoadedAudio }: AudioPlayerProps) {
const urls = useMemo(() => list.map(x => x.audio?.previewUrl), [list]); const audios = useMemo(() => list.map(x => x.audio), [list]);
const audios = useRef(new Map<number, HTMLAudioElement>()); const nodes = useRef(new Map<number, HTMLAudioElement>());
const loaded = useRef(new Set<number>()); const loaded = useRef(new Set<number>());
const nodeEvents = useRef(new Map<number, () => 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(() => { useEffect(() => {
if (playing !== false) { if (playing !== undefined) {
const audio = audios.current.get(playing); const audio = audios[playing], node = nodes.current.get(playing);
if (audio && loaded.current.has(playing)) { if (audio && node && loaded.current.has(playing)) {
if (globalPlaying) globalPlaying.pause(); if (globalPlaying) globalPlaying.pause();
audio.currentTime = 0; node.currentTime = audio.previewStart ? audio.previewStart / 1000 : 0;
audio.volume = DEFAULT_VOLUME; node.volume = BASE_VOLUME * (settings.store.previewVolume / 100);
audio.play().catch(error => { node.play().catch(error => {
showToast("Failed to play song preview!", Toasts.Type.FAILURE); showToast("Failed to play song preview!", Toasts.Type.FAILURE);
logger.error("Failed to play audio", error); logger.error("Failed to play audio", error);
setPlaying(false); setPlaying(undefined);
}); });
globalPlaying = audio; globalPlaying = node;
} else { } else {
setPlaying(false); setPlaying(undefined);
}
} else {
for (const audio of audios.current.values()) {
if (globalPlaying === audio) audio.pause();
} }
} }
for (const audio of audios.current.values()) { for (const [index, node] of nodes.current) {
if (audio !== globalPlaying && !audio.paused) { if (playing !== index && !node.paused) {
audio.pause(); node.pause();
} else if (audio === globalPlaying && audio.paused) { } else if (globalPlaying === node && node.paused) {
globalPlaying = undefined; globalPlaying = undefined;
audioRef.current = undefined; audioRef.current = undefined;
} else if (audio === globalPlaying) { } else if (node === globalPlaying) {
audioRef.current = audio; audioRef.current = node;
} }
} }
}, [playing]); }, [playing, audios]);
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<number>(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]);
return ( return (
<div style={{ display: "none" }} aria-hidden="true"> <div style={{ display: "none" }} aria-hidden="true">
{urls.map((url, i) => {audios.map((audio, index) =>
url && ( audio && (
<audio <AudioItem
key={url} key={audio.previewUrl}
src={url} audio={audio}
preload="metadata" index={index}
ref={audio => handleRef(i, audio)} handleRef={handleRef}
onLoadedData={() => handleLoaded(i)} handleLoaded={handleLoaded}
onError={() => handleErrored(i)} handleStopped={handleStopped}
onEnded={() => handleEnded(i)}
onPause={() => handlePaused(i)}
/> />
) )
)} )}

View file

@ -4,18 +4,22 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { RenderInfoEntry } from "@song-spotlight/api/handlers";
import { useEffect, useMemo, useState } from "@webpack/common"; import { useEffect, useMemo, useState } from "@webpack/common";
import { JSX, RefObject } from "react"; import { JSX, RefObject } from "react";
interface ProgressCircleProps extends SvgProps { interface ProgressCircleProps extends SvgProps {
border: number; border: number;
audioRef: RefObject<HTMLAudioElement | undefined>; audioRef: RefObject<HTMLAudioElement | undefined>;
playingRef: RefObject<RenderInfoEntry | undefined>;
} }
type SvgProps = JSX.IntrinsicElements["svg"]; 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, stroke, circumference } = useMemo(() => {
const radius = 50 - border * 2; const radius = SIZE - border * 2;
return { return {
radius, radius,
stroke: border * 2, stroke: border * 2,
@ -26,9 +30,14 @@ export default function ProgressCircle({ border, audioRef, ...props }: ProgressC
useEffect(() => { useEffect(() => {
let handle = requestAnimationFrame(function update() { let handle = requestAnimationFrame(function update() {
const audio = audioRef.current; const audio = audioRef.current, playing = playingRef.current?.audio;
if (audio && !Number.isNaN(audio.duration) && !audio.paused) { if (audio && playing && !Number.isNaN(audio.duration) && !audio.paused) {
setProgress(audio.currentTime / audio.duration); 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 { } else {
setProgress(0); setProgress(0);
} }
@ -42,11 +51,11 @@ export default function ProgressCircle({ border, audioRef, ...props }: ProgressC
return ( return (
<svg <svg
{...props} {...props}
viewBox="0 0 100 100" viewBox={`0 0 ${SIZE * 2} ${SIZE * 2}`}
> >
<circle <circle
cx={50} cx={SIZE}
cy={50} cy={SIZE}
r={radius} r={radius}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@ -54,7 +63,7 @@ export default function ProgressCircle({ border, audioRef, ...props }: ProgressC
strokeDasharray={circumference} strokeDasharray={circumference}
strokeDashoffset={circumference * (1 - progress)} strokeDashoffset={circumference * (1 - progress)}
strokeLinecap="round" strokeLinecap="round"
transform="rotate(-90 50 50)" transform={`rotate(-90 ${SIZE} ${SIZE})`}
data-empty={progress === 0} data-empty={progress === 0}
/> />
</svg> </svg>

View file

@ -31,18 +31,29 @@ import {
import { DragEvent } from "react"; import { DragEvent } from "react";
interface EditableSongProps { interface EditableSongProps {
index: number;
song: Song; song: Song;
insert?: "top" | "bottom"; insert?: "top" | "bottom";
setSongRef(div: HTMLAnchorElement | null): void; setSongRef(index: number, div: HTMLAnchorElement | null): void;
onDrag(event: DragEvent<HTMLAnchorElement>): void; onDrag(index: number, event: DragEvent<HTMLAnchorElement>): void;
onDrop(song: Song): void; onDrop(song: Song): void;
onRemove(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 { render, failed } = useRender(song);
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const refCallback = useCallback((div: HTMLAnchorElement | null) => setSongRef(index, div), [index, setSongRef]);
const dragCallback = useCallback((event: DragEvent<HTMLAnchorElement>) => {
setDragging(true);
onDrag(index, event);
}, [index, onDrag]);
const dragEndCallback = useCallback(() => {
setDragging(false);
onDrop(song);
}, [song, onDrop]);
return ( return (
<Link <Link
href={render?.link} href={render?.link}
@ -72,17 +83,10 @@ function EditableSong({ song, insert, setSongRef, onDrag, onDrop, onRemove }: Ed
draggable="true" draggable="true"
data-dragging={dragging} data-dragging={dragging}
data-insert={insert} data-insert={insert}
onDragStart={event => { onDragStart={dragCallback}
setDragging(true); onDrag={dragCallback}
onDrag(event); onDragEnd={dragEndCallback}
}} ref={refCallback}
onDrag={onDrag}
onDragEnd={() => {
setDragging(false);
onDrop(song);
}}
useDefaultUnderlineStyles={false}
ref={div => setSongRef(div)}
className={cl("editable-song-container")} className={cl("editable-song-container")}
> >
<Flex alignItems="center" gap="12px" className={cl("editable-song")}> <Flex alignItems="center" gap="12px" className={cl("editable-song")}>
@ -211,10 +215,11 @@ export default function SongList({ localData, setLocalData }: SongListProps) {
if (slot === "song") { if (slot === "song") {
return ( return (
<EditableSong <EditableSong
index={i}
song={song} song={song}
insert={last && insert === i + 1 ? "bottom" : insert === i ? "top" : undefined} insert={last && insert === i + 1 ? "bottom" : insert === i ? "top" : undefined}
setSongRef={element => handleRef(i, element)} setSongRef={handleRef}
onDrag={event => handleDrag(i, event)} onDrag={handleDrag}
onDrop={handleDrop} onDrop={handleDrop}
onRemove={handleRemove} onRemove={handleRemove}
key={sid(song)} key={sid(song)}

View file

@ -17,6 +17,7 @@ import { RenderSongInfo } from "@song-spotlight/api/handlers";
import { UserData } from "@song-spotlight/api/structs"; import { UserData } from "@song-spotlight/api/structs";
import { sid } from "@song-spotlight/api/util"; import { sid } from "@song-spotlight/api/util";
import { classes } from "@utils/index"; import { classes } from "@utils/index";
import { User } from "@vencord/discord-types";
import { import {
SelectedChannelStore, SelectedChannelStore,
SelectedGuildStore, SelectedGuildStore,
@ -27,12 +28,12 @@ import {
useState, useState,
} from "@webpack/common"; } from "@webpack/common";
import { ProfileSongsProps } from "./ProfileSongs";
const shownSongs = 4; const shownSongs = 4;
interface CollapsedProfileSongsProps extends ProfileSongsProps { interface CollapsedProfileSongsProps {
data?: UserData; data?: UserData;
user: User;
isSideBar: boolean;
} }
export default function CollapsedProfileSongs({ data, user, isSideBar }: CollapsedProfileSongsProps) { export default function CollapsedProfileSongs({ data, user, isSideBar }: CollapsedProfileSongsProps) {

View file

@ -39,7 +39,7 @@ import {
import Song from "."; import Song from ".";
import CollapsedProfileSongs from "./CollapsedProfileSongs"; import CollapsedProfileSongs from "./CollapsedProfileSongs";
export interface ProfileSongsProps { interface ProfileSongsProps {
user: User; user: User;
isSideBar: boolean; isSideBar: boolean;
} }
@ -49,7 +49,7 @@ export default function ProfileSongs({ user, isSideBar }: ProfileSongsProps) {
const { isAuthorized } = useAuthorizationStore(); const { isAuthorized } = useAuthorizationStore();
const { users } = useSongStore(); const { users } = useSongStore();
const { profileSongsLimit, collapseSongList } = settings.use(); const { profileSongsLimit, collapseSongList } = settings.use();
const userId = user?.id; const userId = user.id;
const data = users[userId]?.data; const data = users[userId]?.data;
useEffect(() => { useEffect(() => {
if (isAuthorized() && !data) listData(userId).catch(() => setFailed(true)); if (isAuthorized() && !data) listData(userId).catch(() => setFailed(true));

View file

@ -21,7 +21,7 @@ import { JSX } from "react";
import Song from "."; import Song from ".";
export interface WidgetSongsProps { interface WidgetSongsProps {
user: User; user: User;
} }

View file

@ -25,7 +25,7 @@ import AudioPlayer from "@equicordplugins/songSpotlight.desktop/ui/components/Au
import ProgressCircle from "@equicordplugins/songSpotlight.desktop/ui/components/ProgressCircle"; import ProgressCircle from "@equicordplugins/songSpotlight.desktop/ui/components/ProgressCircle";
import ServiceIcon from "@equicordplugins/songSpotlight.desktop/ui/components/ServiceIcon"; import ServiceIcon from "@equicordplugins/songSpotlight.desktop/ui/components/ServiceIcon";
import { openSettingsModal } from "@equicordplugins/songSpotlight.desktop/ui/settings"; 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 { Song as SongType } from "@song-spotlight/api/structs";
import { isListLayout, sid } from "@song-spotlight/api/util"; import { isListLayout, sid } from "@song-spotlight/api/util";
import { copyWithToast } from "@utils/discord"; import { copyWithToast } from "@utils/discord";
@ -43,7 +43,7 @@ import {
useCallback, useCallback,
useMemo, useMemo,
useRef, useRef,
useState, useState
} from "@webpack/common"; } from "@webpack/common";
interface SongEntryProps { interface SongEntryProps {
@ -134,28 +134,29 @@ interface SongInfoProps {
} }
function SongInfo({ owned, song, render, big }: SongInfoProps) { function SongInfo({ owned, song, render, big }: SongInfoProps) {
const [playing, setPlaying] = useState<number | false>(false); const [playing, setPlaying] = useState<number | undefined>(undefined);
const [loaded, setLoaded] = useState(new Set<number>()); const [loaded, setLoaded] = useState(new Set<number>());
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 audios = useMemo(() => render.form === "single" ? [render.single] : render.list, [render]);
const audioRef = useRef<HTMLAudioElement>(undefined); const audioRef = useRef<HTMLAudioElement>(undefined);
const playingRef = useRef<RenderInfoEntry>(undefined);
playingRef.current = playing !== undefined ? audios[playing] : undefined;
const duration = useMemo( const duration = useMemo(
() => { () =>
if (playing !== false) { render.form === "single" ?
return audios[playing].audio?.duration; render.single.audio?.duration :
} else { playing !== undefined ?
return render.form === "single" ? render.single.audio?.duration : undefined; render.list[playing].audio?.duration :
} undefined,
},
[playing, render], [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 baseSize = big ? "md" : "sm";
const subSize = big ? "sm" : "xs"; const subSize = big ? "sm" : "xs";
@ -166,7 +167,7 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) {
list={audios} list={audios}
playing={playing} playing={playing}
setPlaying={setPlaying} setPlaying={setPlaying}
setLoaded={setLoadedAudio} setLoadedAudio={setLoadedAudio}
/> />
<div className={cl("song-grid")}> <div className={cl("song-grid")}>
<Flex gap="8px" alignItems="center" className={cl("song-core")}> <Flex gap="8px" alignItems="center" className={cl("song-core")}>
@ -273,7 +274,7 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) {
alignItems="flex-end" alignItems="flex-end"
gap="6px" gap="6px"
className={cl("song-player")} className={cl("song-player")}
data-idle={playing === false && !big} data-idle={playing === undefined && !big}
> >
{duration && ( {duration && (
<BaseText size={subSize} weight="medium" className={cl("mono", "sub")}> <BaseText size={subSize} weight="medium" className={cl("mono", "sub")}>
@ -282,19 +283,19 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) {
)} )}
<div className={cl("song-progress-container")}> <div className={cl("song-progress-container")}>
<ProgressCircle <ProgressCircle
border={2} border={2.5}
playingRef={playingRef}
audioRef={audioRef} audioRef={audioRef}
className={cl("song-progress")} className={cl("song-progress")}
/> />
<PlayButton <PlayButton
state={playing !== false} state={playing !== undefined}
disabled={loaded.size < 1} disabled={loaded.size < 1}
onClick={() => { onClick={() => {
const loadedI = loaded.values().toArray().sort()[0]; if (playing !== undefined) return setPlaying(undefined);
if (loadedI === undefined) return;
if (playing !== false) setPlaying(false); const loadedIndex = loaded.values().toArray().sort()[0];
else setPlaying(loadedI); if (loadedIndex !== undefined) setPlaying(loadedIndex);
}} }}
/> />
</div> </div>
@ -312,10 +313,8 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) {
isLoaded={loaded.has(i)} isLoaded={loaded.has(i)}
isPlaying={playing === i} isPlaying={playing === i}
onClick={() => { onClick={() => {
if (!loaded.has(i)) return; if (playing === i) setPlaying(undefined);
else if (loaded.has(i)) setPlaying(i);
if (playing !== i) setPlaying(i);
else setPlaying(false);
}} }}
big={big} big={big}
key={entry.link} key={entry.link}