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/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
26
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,5 @@ allowBuilds:
|
||||||
esbuild: true
|
esbuild: true
|
||||||
pmOnFail: warn
|
pmOnFail: warn
|
||||||
verifyDepsBeforeRun: prompt
|
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 { 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();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 />,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { JSX } from "react";
|
||||||
|
|
||||||
import Song from ".";
|
import Song from ".";
|
||||||
|
|
||||||
export interface WidgetSongsProps {
|
interface WidgetSongsProps {
|
||||||
user: User;
|
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 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}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue