Decor: make wysiwyg better

This commit is contained in:
thororen 2026-10-02 10:23:38 -04:00
parent cbab61da91
commit 779a87b9b1
No known key found for this signature in database
3 changed files with 105 additions and 36 deletions

View file

@ -9,6 +9,7 @@ import "./ui/styles.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { User } from "@vencord/discord-types";
import { UserStore } from "@webpack/common"; import { UserStore } from "@webpack/common";
import { CDN_URL, RAW_SKU_ID, setBaseUrl, SKU_ID } from "./lib/constants"; import { CDN_URL, RAW_SKU_ID, setBaseUrl, SKU_ID } from "./lib/constants";
@ -29,6 +30,7 @@ export default definePlugin({
description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.", description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.",
tags: ["Appearance", "Customisation"], tags: ["Appearance", "Customisation"],
authors: [Devs.FieryFlames], authors: [Devs.FieryFlames],
isModified: true,
patches: [ patches: [
// Patch MediaResolver to return correct URL for Decor avatar decorations // Patch MediaResolver to return correct URL for Decor avatar decorations
{ {
@ -122,10 +124,17 @@ export default definePlugin({
find: '("UserProfileModalV2EditingPanel")', find: '("UserProfileModalV2EditingPanel")',
replacement: [ replacement: [
{ {
match: /disabled:\i\|\|\i,errorMessage:\i\}\),/, match: /disabled:(\i\|\|\i),avatarErrorMessage:\i,avatarDecorationErrorMessage:\i\}\),/,
replace: "$&$self.ExperimentDecorSection()," replace: "$&$self.ExperimentDecorSection({disabled:$1}),"
} }
] ]
},
{
find: "isPreviewingMainProfileFallback:",
replacement: {
match: /(?<=avatarDecorationSrc:(\i),.{0,150}userId:(\i)\.id,.{0,100}avatarDecorationOverride:(\i\?\i:\i\?\i:void 0),animateOnHover:!\i\}\))/,
replace: ",vcDecorTileDecoration=($1=$self.useDiscordTileDecorationSrc($2,$1,$3))"
}
} }
], ],
settings, settings,
@ -158,6 +167,11 @@ export default definePlugin({
useUserDecorAvatarDecoration, useUserDecorAvatarDecoration,
useDiscordTileDecorationSrc(user: User, src: string | null, override: unknown) {
const hasDecorDecoration = useUsersDecorationsStore(state => state.getAsset(user.id) != null);
return override === undefined && hasDecorDecoration ? null : src;
},
async start() { async start() {
await setBaseUrl(settings.store.baseUrl); await setBaseUrl(settings.store.baseUrl);
useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true); useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);

View file

@ -104,8 +104,8 @@ export function useUserDecorAvatarDecoration(user?: User): AvatarDecoration | nu
state => { state => {
if (!user) return; if (!user) return;
const newDecorAvatarDecoration = state.getAsset(user.id); const newDecorAvatarDecoration = state.getAsset(user.id);
if (!newDecorAvatarDecoration) return; if (newDecorAvatarDecoration === undefined) return;
if (decorAvatarDecoration !== newDecorAvatarDecoration) setDecorAvatarDecoration(newDecorAvatarDecoration); setDecorAvatarDecoration(newDecorAvatarDecoration);
} }
); );
} catch { } catch {

View file

@ -6,23 +6,76 @@
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { SKU_ID } from "@plugins/decor/lib/constants";
import { useAuthorizationStore } from "@plugins/decor/lib/stores/AuthorizationStore"; import { useAuthorizationStore } from "@plugins/decor/lib/stores/AuthorizationStore";
import { useCurrentUserDecorationsStore } from "@plugins/decor/lib/stores/CurrentUserDecorationsStore"; import { useCurrentUserDecorationsStore } from "@plugins/decor/lib/stores/CurrentUserDecorationsStore";
import { useUsersDecorationsStore } from "@plugins/decor/lib/stores/UsersDecorationsStore";
import { cl } from "@plugins/decor/ui"; import { cl } from "@plugins/decor/ui";
import { openChangeDecorationModal } from "@plugins/decor/ui/modals/ChangeDecorationModal"; import { openChangeDecorationModal } from "@plugins/decor/ui/modals/ChangeDecorationModal";
import { findComponentByCodeLazy } from "@webpack"; import { classes } from "@utils/misc";
import { NewCustomizationSection, useEffect } from "@webpack/common"; import { findByCodeLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { Avatar, NewCustomizationSection, useEffect, UserStore } from "@webpack/common";
import type { ComponentType } from "react";
const CustomizationSection = findComponentByCodeLazy(".DESCRIPTION", "hasBackground:"); const CustomizationSection = findComponentByCodeLazy(".DESCRIPTION", "hasBackground:");
const EditTile = findComponentByCodeLazy(".tileButton,", "affordance:");
const useAvatarWithDecoration = findByCodeLazy("avatarOverride:", "avatarDecorationOverride:", "avatarPlaceholderSrc:");
const TilePreviewClasses = findCssClassesLazy("emptyPreview", "fadeInAvatarImg");
const AvatarWithDecoration = Avatar as ComponentType<any>;
export interface DecorSectionProps { export interface DecorSectionProps {
hideTitle?: boolean; hideTitle?: boolean;
hideDivider?: boolean; hideDivider?: boolean;
noMargin?: boolean; noMargin?: boolean;
useNewSection?: boolean; useNewSection?: boolean;
disabled?: boolean;
} }
export default function DecorSection({ hideTitle = false, hideDivider = false, noMargin = false, useNewSection = false }: DecorSectionProps) { function DecorTilePreview({ asset, isInteracting }: { asset: string | null | undefined; isInteracting: boolean; }) {
const { avatarDecorationSrc, avatarSrc, avatarPlaceholderSrc } = useAvatarWithDecoration({
userId: UserStore.getCurrentUser().id,
size: "SIZE_72",
showPending: true,
avatarDecorationOverride: asset ? { asset, skuId: SKU_ID } : null,
animateOnHover: !isInteracting
});
const showAvatar = avatarDecorationSrc != null && isInteracting;
return (
<AvatarWithDecoration
className={classes(avatarDecorationSrc == null && TilePreviewClasses.emptyPreview)}
src={showAvatar ? avatarSrc : avatarPlaceholderSrc}
imageClassName={showAvatar ? TilePreviewClasses.fadeInAvatarImg : undefined}
avatarDecoration={avatarDecorationSrc}
size="SIZE_72"
aria-hidden
/>
);
}
function DecorTile({ disabled, onClick, onRemove }: { disabled?: boolean; onClick(): void; onRemove?(): void; }) {
const asset = useUsersDecorationsStore(state => state.getAsset(UserStore.getCurrentUser().id));
return (
<EditTile
variant="square"
affordance={onRemove
? { type: "remove", onClick: onRemove, accessibleLabel: "Remove Decor decoration" }
: asset ? undefined : "add"
}
onClick={onClick}
accessibleLabel="Decor"
accessibleValue={asset ? "Decor decoration" : "None"}
aria-haspopup="dialog"
disabled={disabled}
renderPreview={(isInteracting: boolean) => <DecorTilePreview asset={asset} isInteracting={isInteracting} />}
/>
);
}
export default function DecorSection({ hideTitle = false, hideDivider = false, noMargin = false, useNewSection = false, disabled = false }: DecorSectionProps) {
const authorization = useAuthorizationStore(); const authorization = useAuthorizationStore();
const { selectedDecoration, select: selectDecoration, fetch: fetchDecorations } = useCurrentUserDecorationsStore(); const { selectedDecoration, select: selectDecoration, fetch: fetchDecorations } = useCurrentUserDecorationsStore();
@ -30,49 +83,51 @@ export default function DecorSection({ hideTitle = false, hideDivider = false, n
if (authorization.isAuthorized()) fetchDecorations(); if (authorization.isAuthorized()) fetchDecorations();
}, [authorization.token]); }, [authorization.token]);
const NewSection = useNewSection ? NewCustomizationSection : undefined; const openModal = () => {
if (!authorization.isAuthorized()) {
authorization.authorize().then(openChangeDecorationModal).catch(() => { });
} else {
openChangeDecorationModal();
}
};
if (useNewSection && !NewSection) return null; const canRemove = selectedDecoration != null && authorization.isAuthorized();
const Section = (useNewSection ? NewCustomizationSection : CustomizationSection); if (useNewSection) {
const sectionProps = useNewSection if (!NewCustomizationSection) return null;
? { heading: hideTitle ? undefined : "Decor" }
: {
title: hideTitle ? undefined : "Decor",
hasBackground: true,
hideDivider,
className: noMargin ? cl("section-remove-margin") : undefined
};
const changeLabel = useNewSection ? "Change" : "Change Decoration"; return (
const removeLabel = useNewSection ? "Remove" : "Remove Decoration"; <NewCustomizationSection heading={hideTitle ? undefined : "Decor"} disabled={disabled}>
<DecorTile
disabled={disabled}
onClick={openModal}
onRemove={canRemove ? () => selectDecoration(null) : undefined}
/>
</NewCustomizationSection>
);
}
return ( return (
<Section {...sectionProps}> <CustomizationSection
title={hideTitle ? undefined : "Decor"}
hasBackground
hideDivider={hideDivider}
className={noMargin ? cl("section-remove-margin") : undefined}
>
<Flex gap="4px"> <Flex gap="4px">
<Button <Button onClick={openModal} variant="primary" size="small">
onClick={() => { Change Decoration
if (!authorization.isAuthorized()) {
authorization.authorize().then(openChangeDecorationModal).catch(() => { });
} else {
openChangeDecorationModal();
}
}}
variant="primary"
size="small"
>
{changeLabel}
</Button> </Button>
{selectedDecoration && authorization.isAuthorized() && ( {canRemove && (
<Button <Button
onClick={() => selectDecoration(null)} onClick={() => selectDecoration(null)}
variant="secondary" variant="secondary"
size="small" size="small"
> >
{removeLabel} Remove Decoration
</Button> </Button>
)} )}
</Flex> </Flex>
</Section> </CustomizationSection>
); );
} }