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:
parent
cf08239295
commit
8e9d78c3aa
14 changed files with 201 additions and 163 deletions
|
|
@ -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",
|
||||
|
|
|
|||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -17,3 +17,5 @@ allowBuilds:
|
|||
esbuild: true
|
||||
pmOnFail: warn
|
||||
verifyDepsBeforeRun: prompt
|
||||
minimumReleaseAgeExclude:
|
||||
- '@song-spotlight/api@2.2.0'
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: () => <Settings />,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <audio
|
||||
src={audio.previewUrl}
|
||||
preload="metadata"
|
||||
ref={refCallback}
|
||||
onLoadedData={loadedCallback}
|
||||
onError={erroredCallback}
|
||||
onPause={pausedCallback}
|
||||
onEnded={endedCallback}
|
||||
/>;
|
||||
}
|
||||
|
||||
interface AudioPlayerProps {
|
||||
audioRef: RefObject<HTMLAudioElement | undefined>;
|
||||
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<number, HTMLAudioElement>());
|
||||
export default function AudioPlayer({ audioRef, list, playing, setPlaying, setLoadedAudio }: AudioPlayerProps) {
|
||||
const audios = useMemo(() => list.map(x => x.audio), [list]);
|
||||
const nodes = useRef(new Map<number, HTMLAudioElement>());
|
||||
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(() => {
|
||||
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<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]);
|
||||
}, [playing, audios]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "none" }} aria-hidden="true">
|
||||
{urls.map((url, i) =>
|
||||
url && (
|
||||
<audio
|
||||
key={url}
|
||||
src={url}
|
||||
preload="metadata"
|
||||
ref={audio => handleRef(i, audio)}
|
||||
onLoadedData={() => handleLoaded(i)}
|
||||
onError={() => handleErrored(i)}
|
||||
onEnded={() => handleEnded(i)}
|
||||
onPause={() => handlePaused(i)}
|
||||
{audios.map((audio, index) =>
|
||||
audio && (
|
||||
<AudioItem
|
||||
key={audio.previewUrl}
|
||||
audio={audio}
|
||||
index={index}
|
||||
handleRef={handleRef}
|
||||
handleLoaded={handleLoaded}
|
||||
handleStopped={handleStopped}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<HTMLAudioElement | undefined>;
|
||||
playingRef: RefObject<RenderInfoEntry | undefined>;
|
||||
}
|
||||
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 (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 100 100"
|
||||
viewBox={`0 0 ${SIZE * 2} ${SIZE * 2}`}
|
||||
>
|
||||
<circle
|
||||
cx={50}
|
||||
cy={50}
|
||||
cx={SIZE}
|
||||
cy={SIZE}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
|
|
@ -54,7 +63,7 @@ export default function ProgressCircle({ border, audioRef, ...props }: ProgressC
|
|||
strokeDasharray={circumference}
|
||||
strokeDashoffset={circumference * (1 - progress)}
|
||||
strokeLinecap="round"
|
||||
transform="rotate(-90 50 50)"
|
||||
transform={`rotate(-90 ${SIZE} ${SIZE})`}
|
||||
data-empty={progress === 0}
|
||||
/>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -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<HTMLAnchorElement>): void;
|
||||
setSongRef(index: number, div: HTMLAnchorElement | null): void;
|
||||
onDrag(index: number, event: DragEvent<HTMLAnchorElement>): 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<HTMLAnchorElement>) => {
|
||||
setDragging(true);
|
||||
onDrag(index, event);
|
||||
}, [index, onDrag]);
|
||||
const dragEndCallback = useCallback(() => {
|
||||
setDragging(false);
|
||||
onDrop(song);
|
||||
}, [song, onDrop]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={render?.link}
|
||||
|
|
@ -72,17 +83,10 @@ function EditableSong({ song, insert, setSongRef, onDrag, onDrop, onRemove }: Ed
|
|||
draggable="true"
|
||||
data-dragging={dragging}
|
||||
data-insert={insert}
|
||||
onDragStart={event => {
|
||||
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")}
|
||||
>
|
||||
<Flex alignItems="center" gap="12px" className={cl("editable-song")}>
|
||||
|
|
@ -211,10 +215,11 @@ export default function SongList({ localData, setLocalData }: SongListProps) {
|
|||
if (slot === "song") {
|
||||
return (
|
||||
<EditableSong
|
||||
index={i}
|
||||
song={song}
|
||||
insert={last && insert === i + 1 ? "bottom" : insert === i ? "top" : undefined}
|
||||
setSongRef={element => handleRef(i, element)}
|
||||
onDrag={event => handleDrag(i, event)}
|
||||
setSongRef={handleRef}
|
||||
onDrag={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
onRemove={handleRemove}
|
||||
key={sid(song)}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { JSX } from "react";
|
|||
|
||||
import Song from ".";
|
||||
|
||||
export interface WidgetSongsProps {
|
||||
interface WidgetSongsProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<number | false>(false);
|
||||
const [playing, setPlaying] = useState<number | undefined>(undefined);
|
||||
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 audioRef = useRef<HTMLAudioElement>(undefined);
|
||||
const playingRef = useRef<RenderInfoEntry>(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}
|
||||
/>
|
||||
<div className={cl("song-grid")}>
|
||||
<Flex gap="8px" alignItems="center" className={cl("song-core")}>
|
||||
|
|
@ -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 && (
|
||||
<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")}>
|
||||
<ProgressCircle
|
||||
border={2}
|
||||
border={2.5}
|
||||
playingRef={playingRef}
|
||||
audioRef={audioRef}
|
||||
className={cl("song-progress")}
|
||||
/>
|
||||
<PlayButton
|
||||
state={playing !== false}
|
||||
state={playing !== undefined}
|
||||
disabled={loaded.size < 1}
|
||||
onClick={() => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue