SpotifyControls: fix SeekBar not updating (#3381)

Also slightly reworks LazyComponents for more useful typing
This commit is contained in:
Vending Machine 2025-04-14 15:21:30 +02:00 committed by GitHub
parent a8c01a2a05
commit 0f4d3dfd3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 57 additions and 27 deletions

View file

@ -18,7 +18,7 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/react"; import { LazyComponent, LazyComponentWrapper } from "@utils/react";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard"; import { ErrorCard } from "./ErrorCard";
@ -107,9 +107,9 @@ const ErrorBoundary = LazyComponent(() => {
} }
}; };
}) as }) as
React.ComponentType<React.PropsWithChildren<Props>> & { LazyComponentWrapper<React.ComponentType<React.PropsWithChildren<Props>> & {
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>; wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
}; }>;
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => ( ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}> <ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>

View file

@ -87,7 +87,7 @@ async function runReporter() {
result = Webpack[method](...args); result = Webpack[method](...args);
} }
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail"); if (result == null || (result.$$vencordGetWrappedComponent != null && result.$$vencordGetWrappedComponent() == null)) throw new Error("Webpack Find Fail");
} catch (e) { } catch (e) {
let logMessage = searchType; let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {

View file

@ -142,8 +142,7 @@ export default definePlugin({
// Thus, we sanity check webpack modules // Thus, we sanity check webpack modules
Layer(props: LayerProps) { Layer(props: LayerProps) {
try { try {
// @ts-ignore [FocusLock.$$vencordGetWrappedComponent(), ComponentDispatch, Classes].forEach(e => e.test);
[FocusLock.$$vencordInternal(), ComponentDispatch, Classes].forEach(e => e.test);
} catch { } catch {
new Logger("BetterSettings").error("Failed to find some components"); new Logger("BetterSettings").error("Failed to find some components");
return props.children; return props.children;

View file

@ -174,8 +174,8 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
function unwrapProxy(value: any) { function unwrapProxy(value: any) {
if (value[SYM_LAZY_GET]) { if (value[SYM_LAZY_GET]) {
forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]; forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
} else if (value.$$vencordInternal) { } else if (value.$$vencordGetWrappedComponent) {
return forceLoad ? value.$$vencordInternal() : value; return forceLoad ? value.$$vencordGetWrappedComponent() : value;
} }
return value; return value;

View file

@ -28,6 +28,7 @@ import { openImageModal } from "@utils/discord";
import { classes, copyWithToast } from "@utils/misc"; import { classes, copyWithToast } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SeekBar } from "./SeekBar";
import { SpotifyStore, Track } from "./SpotifyStore"; import { SpotifyStore, Track } from "./SpotifyStore";
const cl = classNameFactory("vc-spotify-"); const cl = classNameFactory("vc-spotify-");
@ -160,7 +161,7 @@ const seek = debounce((v: number) => {
SpotifyStore.seek(v); SpotifyStore.seek(v);
}); });
function SeekBar() { function SpotifySeekBar() {
const { duration } = SpotifyStore.track!; const { duration } = SpotifyStore.track!;
const [storePosition, isSettingPosition, isPlaying] = useStateFromStores( const [storePosition, isSettingPosition, isPlaying] = useStateFromStores(
@ -181,6 +182,12 @@ function SeekBar() {
} }
}, [storePosition, isSettingPosition, isPlaying]); }, [storePosition, isSettingPosition, isPlaying]);
const onChange = (v: number) => {
if (isSettingPosition) return;
setPosition(v);
seek(v);
};
return ( return (
<div id={cl("progress-bar")}> <div id={cl("progress-bar")}>
<Forms.FormText <Forms.FormText
@ -190,16 +197,13 @@ function SeekBar() {
> >
{msToHuman(position)} {msToHuman(position)}
</Forms.FormText> </Forms.FormText>
<Menu.MenuSliderControl <SeekBar
initialValue={position}
minValue={0} minValue={0}
maxValue={duration} maxValue={duration}
value={position} onValueChange={onChange}
onChange={(v: number) => { asValueChanges={onChange}
if (isSettingPosition) return; onValueRender={msToHuman}
setPosition(v);
seek(v);
}}
renderValue={msToHuman}
/> />
<Forms.FormText <Forms.FormText
variant="text-xs/medium" variant="text-xs/medium"
@ -382,7 +386,7 @@ export function Player() {
return ( return (
<div id={cl("player")} style={exportTrackImageStyle}> <div id={cl("player")} style={exportTrackImageStyle}>
<Info track={track} /> <Info track={track} />
<SeekBar /> <SpotifySeekBar />
<Controls /> <Controls />
</div> </div>
); );

View file

@ -0,0 +1,25 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { LazyComponent } from "@utils/lazyReact";
import { Slider } from "@webpack/common";
export const SeekBar = LazyComponent(() => {
const SliderClass = Slider.$$vencordGetWrappedComponent();
// Discord's Slider does not update `state.value` when `props.initialValue` changes if state.value is not nullish.
// We extend their class and override their `getDerivedStateFromProps` to update the value
return class SeekBar extends SliderClass {
static getDerivedStateFromProps(props: any, state: any) {
const newState = super.getDerivedStateFromProps!(props, state);
if (newState) {
newState.value = props.initialValue;
}
return newState;
}
};
});

View file

@ -4,26 +4,28 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { ComponentType } from "react"; import type { ComponentType } from "react";
import { makeLazy } from "./lazy"; import { makeLazy } from "./lazy";
const NoopComponent = () => null; const NoopComponent = () => null;
export type LazyComponentWrapper<ComponentType> = ComponentType & { $$vencordGetWrappedComponent(): ComponentType; };
/** /**
* A lazy component. The factory method is called on first render. * A lazy component. The factory method is called on first render.
* @param factory Function returning a Component * @param factory Function returning a Component
* @param attempts How many times to try to get the component before giving up * @param attempts How many times to try to get the component before giving up
* @returns Result of factory function * @returns Result of factory function
*/ */
export function LazyComponent<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) { export function LazyComponent<T extends object = any>(factory: () => ComponentType<T>, attempts = 5): LazyComponentWrapper<ComponentType<T>> {
const get = makeLazy(factory, attempts); const get = makeLazy(factory, attempts);
const LazyComponent = (props: T) => { const LazyComponent = (props: T) => {
const Component = get() ?? NoopComponent; const Component = get() ?? NoopComponent;
return <Component {...props} />; return <Component {...props} />;
}; };
LazyComponent.$$vencordInternal = get; LazyComponent.$$vencordGetWrappedComponent = get;
return LazyComponent as ComponentType<T>; return LazyComponent;
} }

View file

@ -16,19 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { LazyComponent } from "@utils/react"; import { LazyComponent, LazyComponentWrapper } from "@utils/react";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack"; import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack";
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T { export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
let myValue: T = function () { let myValue: T = function () {
throw new Error(`Vencord could not find the ${name} Component`); throw new Error(`Vencord could not find the ${name} Component`);
} as any; } as any;
const lazyComponent = LazyComponent(() => myValue) as T; const lazyComponent = LazyComponent(() => myValue) as LazyComponentWrapper<T>;
waitFor(filter, (v: any) => { waitFor(filter, (v: any) => {
myValue = v; myValue = v;
Object.assign(lazyComponent, v); Object.assign(lazyComponent, v);

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react"; import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react";
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
@ -356,7 +356,7 @@ export type SearchableSelect = ComponentType<PropsWithChildren<{
"aria-labelledby"?: boolean; "aria-labelledby"?: boolean;
}>>; }>>;
export type Slider = ComponentType<PropsWithChildren<{ export type Slider = ComponentClass<PropsWithChildren<{
initialValue: number; initialValue: number;
defaultValue?: number; defaultValue?: number;
keyboardStep?: number; keyboardStep?: number;