ProfileSets Update (: (#733)
* via upload (: * Add files via upload * fix lint error Added import for ProfilePreset from @vencord/discord-types. * fix anchor removal * Update index.tsx * Update index.tsx * booLEANNNN * fix * fix boolean * fix accidental file change * fix issue #thor * better find * fix profile * fix storage * add anchor * top level * fix page keys bad alignment with server-profile page * fix server profile + pronouns not loading & lint errors * fix anchor v2 * fix anchor v3 * anchor * Update index.tsx --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
39ea0e8be6
commit
ae1e5bd1f2
8 changed files with 570 additions and 332 deletions
|
|
@ -17,7 +17,20 @@ interface ConfirmModalProps extends ModalProps {
|
|||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface ImportProfilesModalProps extends ModalProps {
|
||||
title: string;
|
||||
message: string;
|
||||
onOverride: () => void;
|
||||
onMerge: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmModal({ title, message, confirmText, cancelText, onConfirm, onCancel, onClose, transitionState }: ConfirmModalProps) {
|
||||
const closeAfter = (action: () => void) => () => {
|
||||
action();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot transitionState={transitionState}>
|
||||
<ModalHeader>
|
||||
|
|
@ -27,24 +40,42 @@ export function ConfirmModal({ title, message, confirmText, cancelText, onConfir
|
|||
<Paragraph>{message}</Paragraph>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Button onClick={closeAfter(onConfirm)}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" onClick={closeAfter(onCancel)}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export function ImportProfilesModal({ title, message, onOverride, onMerge, onCancel, onClose, transitionState }: ImportProfilesModalProps) {
|
||||
const closeAfter = (action: () => void) => () => {
|
||||
action();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot transitionState={transitionState}>
|
||||
<ModalHeader>
|
||||
<Heading tag="h2">{title}</Heading>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Paragraph>{message}</Paragraph>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button onClick={closeAfter(onOverride)}>
|
||||
Override
|
||||
</Button>
|
||||
<Button onClick={closeAfter(onMerge)}>
|
||||
Merge
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={closeAfter(onCancel)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,34 +5,48 @@
|
|||
*/
|
||||
|
||||
import { classes } from "@utils/misc";
|
||||
import { ProfilePreset } from "@vencord/discord-types";
|
||||
import { ContextMenuApi, Menu, React, showToast, TextInput, Toasts } from "@webpack/common";
|
||||
import { ContextMenuApi, Menu, React, TextInput } from "@webpack/common";
|
||||
|
||||
import { cl } from "..";
|
||||
import { deletePreset, movePreset, renamePreset, updatePresetField } from "../utils/actions";
|
||||
import { getCurrentProfile } from "../utils/profile";
|
||||
import { PresetSection, type ProfilePresetEx } from "../utils/storage";
|
||||
|
||||
interface PresetListProps {
|
||||
presets: ProfilePreset[];
|
||||
allPresets: ProfilePreset[];
|
||||
presets: ProfilePresetEx[];
|
||||
allPresets: ProfilePresetEx[];
|
||||
avatarSize: number;
|
||||
selectedPreset: number;
|
||||
onLoad: (index: number) => void;
|
||||
onUpdate: () => void;
|
||||
guildId?: string;
|
||||
section: PresetSection;
|
||||
currentPage: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
export function PresetList({ presets, allPresets, avatarSize, selectedPreset, onLoad, onUpdate, guildId, currentPage, onPageChange }: PresetListProps) {
|
||||
export function PresetList({
|
||||
presets,
|
||||
allPresets,
|
||||
avatarSize,
|
||||
selectedPreset,
|
||||
onLoad,
|
||||
onUpdate,
|
||||
guildId,
|
||||
section,
|
||||
currentPage,
|
||||
onPageChange
|
||||
}: PresetListProps) {
|
||||
const [renaming, setRenaming] = React.useState<number>(-1);
|
||||
const [renameText, setRenameText] = React.useState("");
|
||||
const isGuildProfile = section === "server";
|
||||
|
||||
return (
|
||||
<div className={cl("list-container")}>
|
||||
{presets.map(preset => {
|
||||
const actualIndex = allPresets.indexOf(preset);
|
||||
const isRenaming = renaming === actualIndex;
|
||||
const isSelected = !isRenaming && selectedPreset === actualIndex;
|
||||
const date = new Date(preset.timestamp);
|
||||
const formattedDate = date.toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
|
|
@ -44,6 +58,13 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
minute: "2-digit"
|
||||
});
|
||||
|
||||
const commitRename = () => {
|
||||
const nextName = renameText.trim();
|
||||
if (!nextName) return;
|
||||
renamePreset(actualIndex, nextName, section, guildId);
|
||||
onUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={actualIndex}
|
||||
|
|
@ -52,17 +73,15 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
onClick={() => {
|
||||
if (!isRenaming) {
|
||||
onLoad(actualIndex);
|
||||
showToast(`Profile "${preset.name}" loaded successfully`, Toasts.Type.SUCCESS);
|
||||
}
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (!isRenaming && (e.key === "Enter" || e.key === " ")) {
|
||||
e.preventDefault();
|
||||
onLoad(actualIndex);
|
||||
showToast(`Profile "${preset.name}" loaded successfully`, Toasts.Type.SUCCESS);
|
||||
}
|
||||
}}
|
||||
className={classes(cl("row"), !isRenaming && selectedPreset === actualIndex ? "selected" : "")}
|
||||
className={classes(cl("row"), isSelected ? "selected" : "")}
|
||||
>
|
||||
<div className={cl("avatar-url")}>
|
||||
{preset.avatarDataUrl && (
|
||||
|
|
@ -79,18 +98,12 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
value={renameText}
|
||||
onChange={setRenameText}
|
||||
onBlur={() => {
|
||||
if (renameText.trim()) {
|
||||
renamePreset(actualIndex, renameText);
|
||||
onUpdate();
|
||||
}
|
||||
commitRename();
|
||||
setRenaming(-1);
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter") {
|
||||
if (renameText.trim()) {
|
||||
renamePreset(actualIndex, renameText);
|
||||
onUpdate();
|
||||
}
|
||||
commitRename();
|
||||
setRenaming(-1);
|
||||
} else if (e.key === "Escape") {
|
||||
setRenaming(-1);
|
||||
|
|
@ -135,13 +148,12 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
id="update"
|
||||
label="Update"
|
||||
action={async () => {
|
||||
const profile = await getCurrentProfile(guildId);
|
||||
const profile = await getCurrentProfile(guildId, { isGuildProfile });
|
||||
Object.entries(profile).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
updatePresetField(actualIndex, key as any, value);
|
||||
updatePresetField(actualIndex, key as any, value, section, guildId);
|
||||
}
|
||||
});
|
||||
showToast("Profile updated successfully", Toasts.Type.SUCCESS);
|
||||
onUpdate();
|
||||
}}
|
||||
/>
|
||||
|
|
@ -151,7 +163,7 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
id="move-up"
|
||||
label="Move Up"
|
||||
action={() => {
|
||||
movePreset(actualIndex, actualIndex - 1);
|
||||
movePreset(actualIndex, actualIndex - 1, section, guildId);
|
||||
onUpdate();
|
||||
}}
|
||||
/>
|
||||
|
|
@ -161,7 +173,7 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
id="move-down"
|
||||
label="Move Down"
|
||||
action={() => {
|
||||
movePreset(actualIndex, actualIndex + 1);
|
||||
movePreset(actualIndex, actualIndex + 1, section, guildId);
|
||||
onUpdate();
|
||||
}}
|
||||
/>
|
||||
|
|
@ -171,7 +183,7 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
id="move-to-page-1"
|
||||
label="Move to Page 1"
|
||||
action={() => {
|
||||
movePreset(actualIndex, 0);
|
||||
movePreset(actualIndex, 0, section, guildId);
|
||||
onPageChange(1);
|
||||
onUpdate();
|
||||
}}
|
||||
|
|
@ -183,7 +195,7 @@ export function PresetList({ presets, allPresets, avatarSize, selectedPreset, on
|
|||
label="Delete"
|
||||
color="danger"
|
||||
action={async () => {
|
||||
await deletePreset(actualIndex);
|
||||
await deletePreset(actualIndex, section, guildId);
|
||||
onUpdate();
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,19 +6,25 @@
|
|||
|
||||
import { Heading } from "@components/Heading";
|
||||
import { Button } from "@components/index";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { React, TextInput } from "@webpack/common";
|
||||
import { React, SelectedGuildStore, TextInput, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { cl, settings } from "../index";
|
||||
import { exportPresets, importPresets, savePreset } from "../utils/actions";
|
||||
import { exportPresets, ImportDecision, importPresets, savePreset } from "../utils/actions";
|
||||
import { loadPresetAsPending } from "../utils/profile";
|
||||
import { presets, setCurrentPresetIndex } from "../utils/storage";
|
||||
import { ConfirmModal } from "./confirmModal";
|
||||
import { loadPresets, presets, PresetSection, setCurrentPresetIndex } from "../utils/storage";
|
||||
import { ImportProfilesModal } from "./confirmModal";
|
||||
import { PresetList } from "./presetList";
|
||||
|
||||
const PRESETS_PER_PAGE = 5;
|
||||
|
||||
export function PresetManager({ guildId }: { guildId?: string; }) {
|
||||
type PresetManagerProps = {
|
||||
section?: PresetSection;
|
||||
guildId?: string;
|
||||
};
|
||||
|
||||
export function PresetManager({ section, guildId }: PresetManagerProps) {
|
||||
const [presetName, setPresetName] = React.useState("");
|
||||
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
|
|
@ -26,10 +32,34 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
|
|||
const [pageInput, setPageInput] = React.useState("1");
|
||||
const [selectedPreset, setSelectedPreset] = React.useState<number>(-1);
|
||||
const [searchMode, setSearchMode] = React.useState(false);
|
||||
const lastRandomIndexRef = React.useRef<number>(-1);
|
||||
const resolvedSection: PresetSection = section ?? "main";
|
||||
const isServerSection = resolvedSection === "server";
|
||||
const lastSelectedGuildId = useStateFromStores(
|
||||
[SelectedGuildStore],
|
||||
() => SelectedGuildStore.getLastSelectedGuildId() ?? SelectedGuildStore.getGuildId()
|
||||
);
|
||||
const resolvedGuildId = isServerSection ? (guildId ?? lastSelectedGuildId ?? undefined) : undefined;
|
||||
const canUseGuild = !isServerSection || Boolean(resolvedGuildId);
|
||||
|
||||
const filteredPresets = searchMode
|
||||
? presets.filter(p => p.name.toLowerCase().includes(presetName.toLowerCase()))
|
||||
: presets;
|
||||
React.useEffect(() => {
|
||||
let isActive = true;
|
||||
(async () => {
|
||||
await loadPresets(resolvedSection);
|
||||
if (!isActive) return;
|
||||
setSelectedPreset(-1);
|
||||
setCurrentPage(1);
|
||||
setPageInput("1");
|
||||
forceUpdate();
|
||||
})();
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [resolvedGuildId, resolvedSection]);
|
||||
|
||||
const filteredPresets = !searchMode
|
||||
? presets
|
||||
: presets.filter(preset => preset.name.toLowerCase().includes(presetName.toLowerCase()));
|
||||
|
||||
const totalPages = Math.ceil(filteredPresets.length / PRESETS_PER_PAGE);
|
||||
const startIndex = (currentPage - 1) * PRESETS_PER_PAGE;
|
||||
|
|
@ -43,9 +73,11 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
|
|||
};
|
||||
|
||||
const handleSavePreset = async () => {
|
||||
if (!presetName.trim()) return;
|
||||
if (!canUseGuild) return;
|
||||
const trimmedName = presetName.trim();
|
||||
if (!trimmedName) return;
|
||||
setIsSaving(true);
|
||||
await savePreset(presetName.trim(), guildId);
|
||||
await savePreset(trimmedName, resolvedSection, resolvedGuildId);
|
||||
setPresetName("");
|
||||
setIsSaving(false);
|
||||
const newTotalPages = Math.ceil(presets.length / PRESETS_PER_PAGE);
|
||||
|
|
@ -53,24 +85,53 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
|
|||
forceUpdate();
|
||||
};
|
||||
|
||||
const handleLoadPreset = (index: number) => {
|
||||
const applyPreset = (index: number) => {
|
||||
setSelectedPreset(index);
|
||||
setCurrentPresetIndex(index);
|
||||
loadPresetAsPending(presets[index], guildId);
|
||||
loadPresetAsPending(presets[index], resolvedGuildId, {
|
||||
isGuildProfile: resolvedSection === "server"
|
||||
});
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
const showOverridePrompt = (): Promise<boolean> => {
|
||||
const handleLoadPreset = (index: number) => {
|
||||
if (!canUseGuild) return;
|
||||
applyPreset(index);
|
||||
};
|
||||
|
||||
const selectRandomPreset = (sectionType: PresetSection) => {
|
||||
const availablePresets = presets;
|
||||
if (!availablePresets.length) return null;
|
||||
const randomIndex = Math.floor(Math.random() * availablePresets.length);
|
||||
return { preset: availablePresets[randomIndex], index: randomIndex, section: sectionType };
|
||||
};
|
||||
|
||||
const handleRandomPreset = () => {
|
||||
if (!canUseGuild) return;
|
||||
const selection = selectRandomPreset(resolvedSection);
|
||||
if (!selection) return;
|
||||
let nextIndex = selection.index;
|
||||
if (presets.length > 1 && nextIndex === lastRandomIndexRef.current) {
|
||||
let attempts = 0;
|
||||
while (attempts < 5 && nextIndex === lastRandomIndexRef.current) {
|
||||
nextIndex = Math.floor(Math.random() * presets.length);
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
lastRandomIndexRef.current = nextIndex;
|
||||
applyPreset(nextIndex);
|
||||
};
|
||||
|
||||
const showImportPrompt = (existingCount: number): Promise<ImportDecision> => {
|
||||
return new Promise(resolve => {
|
||||
openModal(props => (
|
||||
<ConfirmModal
|
||||
<ImportProfilesModal
|
||||
{...props}
|
||||
title="Override Existing Presets?"
|
||||
message="Some imported profiles match existing ones. Do you want to override all profiles or combine them?"
|
||||
confirmText="Override"
|
||||
cancelText="Combine"
|
||||
onConfirm={() => resolve(true)}
|
||||
onCancel={() => resolve(false)}
|
||||
title="Import Profiles"
|
||||
message={`You have ${existingCount} existing profiles in this section. Do you want to override them or merge with imported profiles?`}
|
||||
onOverride={() => resolve("override")}
|
||||
onMerge={() => resolve("merge")}
|
||||
onCancel={() => resolve("cancel")}
|
||||
/>
|
||||
));
|
||||
});
|
||||
|
|
@ -81,7 +142,7 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
|
|||
const shouldShowPagination = filteredPresets.length > PRESETS_PER_PAGE;
|
||||
|
||||
return (
|
||||
<div className={cl("section")} >
|
||||
<div className={classes(cl("section"), isServerSection ? cl("section-server") : "")} >
|
||||
<Heading tag="h3" className={cl("heading")}>
|
||||
Saved Profiles
|
||||
</Heading>
|
||||
|
|
@ -99,7 +160,7 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
|
|||
{!searchMode && (
|
||||
<Button
|
||||
size="small"
|
||||
disabled={isSaving || !presetName.trim()}
|
||||
disabled={isSaving || !presetName.trim() || !canUseGuild}
|
||||
onClick={handleSavePreset}
|
||||
className={cl("search-button")}
|
||||
>
|
||||
|
|
@ -128,8 +189,17 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
|
|||
avatarSize={avatarSize}
|
||||
selectedPreset={selectedPreset}
|
||||
onLoad={handleLoadPreset}
|
||||
onUpdate={forceUpdate}
|
||||
guildId={guildId}
|
||||
onUpdate={() => {
|
||||
const newTotal = Math.ceil(presets.length / PRESETS_PER_PAGE);
|
||||
if (newTotal === 0) {
|
||||
handlePageChange(1);
|
||||
} else if (currentPage > newTotal) {
|
||||
handlePageChange(newTotal);
|
||||
}
|
||||
forceUpdate();
|
||||
}}
|
||||
guildId={resolvedGuildId}
|
||||
section={resolvedSection}
|
||||
currentPage={currentPage}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
|
|
@ -173,26 +243,48 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className={cl("import")}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => importPresets(forceUpdate, showOverridePrompt)}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={exportPresets}
|
||||
>
|
||||
Export All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr className={cl("block")} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={cl("import")}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={handleRandomPreset}
|
||||
className={cl("random")}
|
||||
disabled={!presets.length || !canUseGuild}
|
||||
>
|
||||
<span className={cl("random-content")}>
|
||||
<svg className={cl("random-icon")} viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.5 4h-5a1 1 0 0 0 0 2h2.586l-4.293 4.293a1 1 0 0 0 1.414 1.414L19.5 7.414V10a1 1 0 1 0 2 0V5a1 1 0 0 0-1-1Zm-16 1a1 1 0 0 0 0 2h2.586l4.293 4.293a1 1 0 1 0 1.414-1.414L8.414 5H4.5ZM5 15a1 1 0 0 0-1 1v5a1 1 0 1 0 2 0v-2.586l4.293 4.293a1 1 0 0 0 1.414-1.414L7.414 17H10a1 1 0 1 0 0-2H5Zm14.5 0a1 1 0 0 0 0 2h-2.586l-4.293 4.293a1 1 0 1 0 1.414 1.414L18.586 17H21a1 1 0 1 0 0-2h-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
Random
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => importPresets(forceUpdate, showImportPrompt, resolvedSection, resolvedGuildId)}
|
||||
disabled={!canUseGuild}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => exportPresets(resolvedSection)}
|
||||
>
|
||||
Export All
|
||||
</Button>
|
||||
</div>
|
||||
<hr className={cl("block")} />
|
||||
{resolvedSection === "server" && (
|
||||
<hr className={cl("block")} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,20 +9,18 @@ import "./styles.css";
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin, { makeRange, OptionType } from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import { PresetManager } from "./components/presetManager";
|
||||
import { loadPresets } from "./utils/storage";
|
||||
import { loadPresets, PresetSection } from "./utils/storage";
|
||||
|
||||
export const cl = classNameFactory("vc-profile-presets-");
|
||||
export const settings = definePluginSettings({
|
||||
avatarSize: {
|
||||
type: OptionType.SLIDER,
|
||||
type: OptionType.NUMBER,
|
||||
description: "Avatar size in preset list.",
|
||||
default: 40,
|
||||
markers: makeRange(32, 96, 8),
|
||||
stickToMarkers: true
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -36,14 +34,21 @@ export default definePlugin({
|
|||
find: "DefaultCustomizationSections: user cannot be undefined",
|
||||
replacement: {
|
||||
match: /return.{0,50}children:\[(?<=\.getLegacyUsername\(\).*?)/,
|
||||
replace: "$&$self.renderPresetSection(),"
|
||||
replace: "$&$self.renderPresetSection(\"main\"),"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "USER_SETTINGS_GUILD_PROFILE)",
|
||||
replacement: {
|
||||
match: /onChange:\i\}\)(?=.{0,25}profilePreviewTitle:)/,
|
||||
replace: '$&,$self.renderPresetSection("server")'
|
||||
}
|
||||
}
|
||||
],
|
||||
start() {
|
||||
loadPresets();
|
||||
loadPresets("main");
|
||||
},
|
||||
renderPresetSection() {
|
||||
return <PresetManager />;
|
||||
renderPresetSection(section: PresetSection, guildId?: string) {
|
||||
return <PresetManager section={section} guildId={guildId} />;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
margin-bottom: 20px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
max-width: 660px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.vc-profile-presets-section-server {
|
||||
max-width: 660px;
|
||||
}
|
||||
|
||||
.vc-profile-presets-heading {
|
||||
|
|
@ -17,6 +25,7 @@
|
|||
.vc-profile-presets-text-input {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vc-profile-presets-search {
|
||||
|
|
@ -38,6 +47,11 @@
|
|||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.vc-profile-presets-section-server .vc-profile-presets-pagination {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.vc-profile-presets-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -53,9 +67,32 @@
|
|||
.vc-profile-presets-import {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vc-profile-presets-random {
|
||||
background: linear-gradient(135deg, #ff5f6d 0%, #ffc371 50%, #8a7bff 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vc-profile-presets-random:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.vc-profile-presets-random-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.vc-profile-presets-random-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.vc-profile-presets-block {
|
||||
margin: 16px 0;
|
||||
border: none;
|
||||
|
|
@ -91,6 +128,10 @@
|
|||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--background-modifier-accent);
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.vc-profile-presets-row {
|
||||
|
|
@ -104,6 +145,10 @@
|
|||
transition: all 0.15s ease;
|
||||
border: 2px solid transparent;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.vc-profile-presets-row:hover {
|
||||
|
|
@ -161,4 +206,4 @@
|
|||
border: 2px solid var(--background-modifier-accent);
|
||||
border-radius: 4px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,24 +5,22 @@
|
|||
*/
|
||||
|
||||
import { ProfilePreset } from "@vencord/discord-types";
|
||||
import { showToast, Toasts } from "@webpack/common";
|
||||
|
||||
import { getCurrentProfile } from "./profile";
|
||||
import { addPreset, movePresetInArray, presets, removePreset, replaceAllPresets, savePresetsData, updatePreset } from "./storage";
|
||||
import { addPreset, movePresetInArray, PresetSection, presets, removePreset, replaceAllPresets, savePresetsData, updatePreset, type ProfilePresetEx } from "./storage";
|
||||
|
||||
export async function savePreset(name: string, guildId?: string) {
|
||||
const profile = await getCurrentProfile(guildId);
|
||||
const newPreset: ProfilePreset = {
|
||||
export async function savePreset(name: string, section: PresetSection, guildId?: string) {
|
||||
const profile = await getCurrentProfile(guildId, { isGuildProfile: section === "server" });
|
||||
const newPreset: ProfilePresetEx = {
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
...profile,
|
||||
};
|
||||
addPreset(newPreset);
|
||||
await savePresetsData();
|
||||
showToast(`Profile "${name}" saved successfully`, Toasts.Type.SUCCESS);
|
||||
await savePresetsData(section);
|
||||
}
|
||||
|
||||
export async function updatePresetField(index: number, field: keyof Omit<ProfilePreset, "name" | "timestamp">, value: any) {
|
||||
export async function updatePresetField(index: number, field: keyof Omit<ProfilePreset, "name" | "timestamp">, value: any, section: PresetSection, guildId?: string) {
|
||||
if (index < 0 || index >= presets.length) return;
|
||||
|
||||
const updatedPreset = {
|
||||
|
|
@ -31,91 +29,74 @@ export async function updatePresetField(index: number, field: keyof Omit<Profile
|
|||
timestamp: Date.now()
|
||||
};
|
||||
updatePreset(index, updatedPreset);
|
||||
await savePresetsData();
|
||||
await savePresetsData(section);
|
||||
}
|
||||
|
||||
export async function deletePreset(index: number) {
|
||||
export async function deletePreset(index: number, section: PresetSection, guildId?: string) {
|
||||
if (index < 0 || index >= presets.length) return;
|
||||
|
||||
const preset = presets[index];
|
||||
removePreset(index);
|
||||
await savePresetsData();
|
||||
showToast(`Profile "${preset.name}" deleted`, Toasts.Type.MESSAGE);
|
||||
await savePresetsData(section);
|
||||
}
|
||||
|
||||
export async function movePreset(fromIndex: number, toIndex: number) {
|
||||
export async function movePreset(fromIndex: number, toIndex: number, section: PresetSection, guildId?: string) {
|
||||
if (fromIndex < 0 || fromIndex >= presets.length || toIndex < 0 || toIndex >= presets.length) return;
|
||||
|
||||
movePresetInArray(fromIndex, toIndex);
|
||||
await savePresetsData();
|
||||
await savePresetsData(section);
|
||||
}
|
||||
|
||||
export async function renamePreset(index: number, newName: string) {
|
||||
export async function renamePreset(index: number, newName: string, section: PresetSection, guildId?: string) {
|
||||
if (index < 0 || index >= presets.length || !newName.trim()) return;
|
||||
|
||||
const updatedPreset = { ...presets[index], name: newName.trim() };
|
||||
updatePreset(index, updatedPreset);
|
||||
await savePresetsData();
|
||||
showToast(`Profile renamed to "${newName}"`, Toasts.Type.SUCCESS);
|
||||
await savePresetsData(section);
|
||||
}
|
||||
|
||||
export function exportPresets() {
|
||||
export function exportPresets(section: PresetSection) {
|
||||
try {
|
||||
const dataStr = JSON.stringify(presets, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: "application/json" });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `profile-presets-${Date.now()}.json`;
|
||||
link.download = `profile-presets-${section}-${Date.now()}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast("Profiles exported successfully", Toasts.Type.SUCCESS);
|
||||
} catch (err) {
|
||||
showToast("Failed to export profiles", Toasts.Type.FAILURE);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
function presetsAreEqual(preset1: ProfilePreset, preset2: ProfilePreset): boolean {
|
||||
return preset1.avatarDataUrl === preset2.avatarDataUrl &&
|
||||
preset1.bannerDataUrl === preset2.bannerDataUrl &&
|
||||
preset1.bio === preset2.bio &&
|
||||
preset1.pronouns === preset2.pronouns &&
|
||||
JSON.stringify(preset1.themeColors) === JSON.stringify(preset2.themeColors) &&
|
||||
JSON.stringify(preset1.avatarDecoration) === JSON.stringify(preset2.avatarDecoration) &&
|
||||
JSON.stringify(preset1.profileEffect) === JSON.stringify(preset2.profileEffect) &&
|
||||
JSON.stringify(preset1.nameplate) === JSON.stringify(preset2.nameplate);
|
||||
}
|
||||
export type ImportDecision = "override" | "merge" | "cancel";
|
||||
|
||||
export async function importPresets(forceUpdate: () => void, onOverridePrompt: () => Promise<boolean>) {
|
||||
export async function importPresets(
|
||||
forceUpdate: () => void,
|
||||
onImportPrompt: (existingCount: number) => Promise<ImportDecision>,
|
||||
section: PresetSection,
|
||||
guildId?: string
|
||||
) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "application/json";
|
||||
input.onchange = async (e: any) => {
|
||||
input.onchange = async (event: Event) => {
|
||||
try {
|
||||
const file = e.target.files[0];
|
||||
const target = event.currentTarget as HTMLInputElement | null;
|
||||
const file = target?.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const text = await file.text();
|
||||
const importedPresets = JSON.parse(text);
|
||||
|
||||
if (!Array.isArray(importedPresets)) {
|
||||
showToast("Invalid profile file format", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (presets.length > 0) {
|
||||
const hasDuplicates = importedPresets.some(imported =>
|
||||
presets.some(existing => presetsAreEqual(imported, existing))
|
||||
);
|
||||
|
||||
if (hasDuplicates) {
|
||||
const shouldOverride = await onOverridePrompt();
|
||||
if (shouldOverride) {
|
||||
replaceAllPresets(importedPresets);
|
||||
} else {
|
||||
const combined = [...presets, ...importedPresets];
|
||||
replaceAllPresets(combined);
|
||||
}
|
||||
const decision = await onImportPrompt(presets.length);
|
||||
if (decision === "cancel") return;
|
||||
if (decision === "override") {
|
||||
replaceAllPresets(importedPresets);
|
||||
} else {
|
||||
const combined = [...presets, ...importedPresets];
|
||||
replaceAllPresets(combined);
|
||||
|
|
@ -124,11 +105,9 @@ export async function importPresets(forceUpdate: () => void, onOverridePrompt: (
|
|||
replaceAllPresets(importedPresets);
|
||||
}
|
||||
|
||||
await savePresetsData();
|
||||
await savePresetsData(section);
|
||||
forceUpdate();
|
||||
showToast(`Imported ${importedPresets.length} profiles successfully`, Toasts.Type.SUCCESS);
|
||||
} catch (err) {
|
||||
showToast("Failed to import profiles", Toasts.Type.FAILURE);
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
|
|
|
|||
|
|
@ -5,81 +5,165 @@
|
|||
*/
|
||||
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { CustomStatus, ProfileEffect, ProfilePreset } from "@vencord/discord-types";
|
||||
import { CustomStatus, DisplayNameStyles, Nameplate, ProfileEffect, ProfilePreset } from "@vencord/discord-types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher, IconUtils, UserProfileStore, UserStore } from "@webpack/common";
|
||||
import { FluxDispatcher, GuildMemberStore,IconUtils, UserProfileStore, UserStore } from "@webpack/common";
|
||||
|
||||
const logger = new Logger("ProfilePresets");
|
||||
const UserProfileSettingsStore = findStoreLazy("UserProfileSettingsStore");
|
||||
const CustomStatusSettings = getUserSettingLazy("status", "customStatus")!;
|
||||
|
||||
function dispatch(type: string, payload: any) {
|
||||
type PendingChanges = Record<string, unknown> & {
|
||||
pendingAvatar?: ImageInput;
|
||||
avatar?: ImageInput;
|
||||
pendingBanner?: ImageInput;
|
||||
banner?: ImageInput;
|
||||
pendingAvatarDecoration?: { asset: string; skuId: string; } | null;
|
||||
pendingProfileEffect?: ProfileEffect | null;
|
||||
pendingNameplate?: Nameplate | null;
|
||||
pendingDisplayNameStyles?: DisplayNameStyles | null;
|
||||
pendingAccentColor?: number | null;
|
||||
pendingThemeColors?: number[] | null;
|
||||
pendingBio?: string | null;
|
||||
pendingPronouns?: string | null;
|
||||
pendingNickname?: string | null;
|
||||
pendingGlobalName?: string | null;
|
||||
pendingPrimaryGuildId?: string | null;
|
||||
};
|
||||
|
||||
type UserExtras = {
|
||||
collectibles?: {
|
||||
nameplate?: Nameplate;
|
||||
};
|
||||
displayNameStyles?: DisplayNameStyles;
|
||||
avatarDecorationData?: {
|
||||
asset: string;
|
||||
skuId: string;
|
||||
} | null;
|
||||
primaryGuild?: {
|
||||
identityGuildId?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type ProfileCollectible = {
|
||||
skuId: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
accessibilityLabel?: string;
|
||||
reducedMotionSrc?: string;
|
||||
thumbnailPreviewSrc?: string;
|
||||
effects?: ProfileEffect["effects"];
|
||||
animationType?: number;
|
||||
staticFrameSrc?: string;
|
||||
type?: number;
|
||||
};
|
||||
|
||||
type ImageInput = string | { imageUri: string; } | null | undefined;
|
||||
|
||||
function dispatch(type: string, payload: Record<string, unknown>) {
|
||||
FluxDispatcher.dispatch({ type, ...payload });
|
||||
}
|
||||
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.length > 0;
|
||||
}
|
||||
|
||||
function hasImageInput(value: ImageInput): boolean {
|
||||
if (!value) return false;
|
||||
if (typeof value === "string") return value.length > 0;
|
||||
return typeof value === "object" && "imageUri" in value && isNonEmptyString(value.imageUri);
|
||||
}
|
||||
|
||||
export async function imageUrlToBase64(url: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
return new Promise((resolve, reject) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Failed to convert image to base64", err);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function processImage(imageData: any, userId: string, type: "avatar" | "banner"): Promise<string | null> {
|
||||
async function processImage(imageData: ImageInput, userId: string, type: "avatar" | "banner", guildId?: string, useGuildPath?: boolean): Promise<string | null> {
|
||||
if (!imageData) return null;
|
||||
|
||||
if (typeof imageData === "object" && imageData.imageUri) {
|
||||
if (typeof imageData === "object" && "imageUri" in imageData && isNonEmptyString(imageData.imageUri)) {
|
||||
return imageData.imageUri;
|
||||
}
|
||||
|
||||
if (typeof imageData === "string") {
|
||||
if (imageData.startsWith("data:")) return imageData;
|
||||
if (/^https?:\/\//.test(imageData)) {
|
||||
return await imageUrlToBase64(imageData);
|
||||
}
|
||||
|
||||
const isAnimated = imageData.startsWith("a_");
|
||||
const size = type === "banner" ? 1024 : 512;
|
||||
const urlPath = type === "banner" ? "banners" : "avatars";
|
||||
const url = `https://cdn.discordapp.com/${urlPath}/${userId}/${imageData}.${isAnimated ? "gif" : "png"}?size=${size}`;
|
||||
return await imageUrlToBase64(url);
|
||||
const guildPath = guildId ? `guilds/${guildId}/users/${userId}/${type === "banner" ? "banners" : "avatars"}` : urlPath;
|
||||
const guildUrl = `https://cdn.discordapp.com/${guildPath}/${imageData}.${isAnimated ? "gif" : "png"}?size=${size}`;
|
||||
const globalUrl = `https://cdn.discordapp.com/${urlPath}/${userId}/${imageData}.${isAnimated ? "gif" : "png"}?size=${size}`;
|
||||
if (useGuildPath && guildId) {
|
||||
const guildResult = await imageUrlToBase64(guildUrl);
|
||||
if (guildResult) return guildResult;
|
||||
}
|
||||
return await imageUrlToBase64(globalUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getCurrentProfile(guildId?: string): Promise<Omit<ProfilePreset, "name" | "timestamp">> {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const userProfile = UserProfileStore.getUserProfile(currentUser.id);
|
||||
const userAny = currentUser as any;
|
||||
type CurrentProfileOptions = {
|
||||
isGuildProfile?: boolean;
|
||||
};
|
||||
|
||||
const pendingChanges = UserProfileSettingsStore.getPendingChanges(guildId);
|
||||
type LoadPresetOptions = {
|
||||
skipGlobalName?: boolean;
|
||||
skipBio?: boolean;
|
||||
skipPronouns?: boolean;
|
||||
isGuildProfile?: boolean;
|
||||
};
|
||||
|
||||
export async function getCurrentProfile(guildId?: string, options: CurrentProfileOptions = {}): Promise<Omit<ProfilePreset, "name" | "timestamp">> {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const baseProfile = UserProfileStore.getUserProfile(currentUser.id);
|
||||
const isGuildProfile = options.isGuildProfile ?? Boolean(guildId);
|
||||
const effectiveGuildId = isGuildProfile ? guildId : undefined;
|
||||
const guildProfile = effectiveGuildId ? UserProfileStore.getGuildMemberProfile(currentUser.id, effectiveGuildId) : null;
|
||||
const userProfile = guildProfile ?? baseProfile;
|
||||
const userAny = currentUser as UserExtras;
|
||||
const guildMember = effectiveGuildId ? GuildMemberStore.getMember(effectiveGuildId, currentUser.id) : null;
|
||||
|
||||
const pendingChangesDefault = (UserProfileSettingsStore.getPendingChanges() ?? {}) as PendingChanges;
|
||||
const pendingChangesForGuild = (effectiveGuildId
|
||||
? (UserProfileSettingsStore.getPendingChanges(effectiveGuildId) ?? {})
|
||||
: {}) as PendingChanges;
|
||||
const pendingChanges: PendingChanges = isGuildProfile && Object.keys(pendingChangesForGuild).length > 0
|
||||
? pendingChangesForGuild
|
||||
: pendingChangesDefault;
|
||||
|
||||
const customStatusSetting = CustomStatusSettings.getSetting();
|
||||
const customStatus: CustomStatus | null = customStatusSetting ? {
|
||||
text: customStatusSetting.text,
|
||||
emojiId: customStatusSetting.emojiId,
|
||||
emojiName: customStatusSetting.emojiName,
|
||||
expiresAtMs: customStatusSetting.expiresAtMs
|
||||
} : null;
|
||||
const customStatus: CustomStatus | null = isGuildProfile
|
||||
? null
|
||||
: {
|
||||
text: customStatusSetting?.text ?? "",
|
||||
emojiId: customStatusSetting?.emojiId ?? "0",
|
||||
emojiName: customStatusSetting?.emojiName ?? "",
|
||||
expiresAtMs: customStatusSetting?.expiresAtMs ?? "0"
|
||||
};
|
||||
|
||||
const avatarDecoration = (pendingChanges.pendingAvatarDecoration !== undefined
|
||||
? pendingChanges.pendingAvatarDecoration
|
||||
: userAny.avatarDecorationData) ? {
|
||||
asset: (pendingChanges.pendingAvatarDecoration || userAny.avatarDecorationData).asset,
|
||||
skuId: (pendingChanges.pendingAvatarDecoration || userAny.avatarDecorationData).skuId
|
||||
const avatarDecorationSource = pendingChanges.pendingAvatarDecoration ?? userAny.avatarDecorationData;
|
||||
const avatarDecoration = avatarDecorationSource ? {
|
||||
asset: avatarDecorationSource.asset,
|
||||
skuId: avatarDecorationSource.skuId
|
||||
} : null;
|
||||
|
||||
let profileEffect: ProfileEffect | null = null;
|
||||
const effectToUse = pendingChanges.pendingProfileEffect !== undefined
|
||||
? pendingChanges.pendingProfileEffect
|
||||
: userProfile?.profileEffect;
|
||||
const effectToUse = pendingChanges.pendingProfileEffect ?? userProfile?.profileEffect;
|
||||
|
||||
if (effectToUse) {
|
||||
if (effectToUse.skuId && effectToUse.effects) {
|
||||
|
|
@ -96,7 +180,8 @@ export async function getCurrentProfile(guildId?: string): Promise<Omit<ProfileP
|
|||
type: effectToUse.type || 1
|
||||
};
|
||||
} else if (effectToUse.skuId) {
|
||||
const collectible = userProfile?.collectibles?.find((c: any) => c.skuId === effectToUse.skuId);
|
||||
const collectibles = (userProfile as { collectibles?: ProfileCollectible[]; } | null)?.collectibles;
|
||||
const collectible = collectibles?.find(c => c?.skuId === effectToUse.skuId);
|
||||
if (collectible) {
|
||||
profileEffect = {
|
||||
skuId: collectible.skuId,
|
||||
|
|
@ -114,61 +199,65 @@ export async function getCurrentProfile(guildId?: string): Promise<Omit<ProfileP
|
|||
}
|
||||
}
|
||||
|
||||
const nameplateToUse = pendingChanges.pendingNameplate !== undefined
|
||||
? pendingChanges.pendingNameplate
|
||||
: userAny.collectibles?.nameplate;
|
||||
|
||||
const nameplateToUse = pendingChanges.pendingNameplate ?? userAny.collectibles?.nameplate;
|
||||
const nameplate = nameplateToUse ? {
|
||||
skuId: nameplateToUse.skuId,
|
||||
asset: nameplateToUse.asset,
|
||||
label: nameplateToUse.label,
|
||||
palette: nameplateToUse.palette,
|
||||
palette: typeof nameplateToUse.palette === "string" ? nameplateToUse.palette : undefined,
|
||||
type: nameplateToUse.type || 2
|
||||
} : null;
|
||||
|
||||
const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles !== undefined
|
||||
? pendingChanges.pendingDisplayNameStyles
|
||||
: userAny.displayNameStyles;
|
||||
|
||||
const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? userAny.displayNameStyles;
|
||||
const displayNameStyles = displayNameStylesToUse ? {
|
||||
font_id: displayNameStylesToUse.font_id,
|
||||
effect_id: displayNameStylesToUse.effect_id,
|
||||
font_id: displayNameStylesToUse.font_id ?? 0,
|
||||
effect_id: displayNameStylesToUse.effect_id ?? 0,
|
||||
colors: Array.isArray(displayNameStylesToUse.colors) ? displayNameStylesToUse.colors : []
|
||||
} : null;
|
||||
|
||||
const avatarToUse = pendingChanges.pendingAvatar !== undefined
|
||||
? pendingChanges.pendingAvatar
|
||||
: currentUser.avatar;
|
||||
const pendingAvatar = pendingChanges.pendingAvatar ?? pendingChanges.avatar;
|
||||
const avatarToUse: ImageInput = hasImageInput(pendingAvatar)
|
||||
? pendingAvatar
|
||||
: (isGuildProfile ? (guildMember?.avatar ?? currentUser.avatar ?? null) : (currentUser.avatar ?? null));
|
||||
|
||||
const avatarDataUrl = await processImage(avatarToUse, currentUser.id, "avatar")
|
||||
|| IconUtils.getDefaultAvatarURL(currentUser.id);
|
||||
const useGuildAvatar = !!(effectiveGuildId && isGuildProfile && guildMember?.avatar && avatarToUse === guildMember.avatar);
|
||||
|
||||
const bannerToUse = pendingChanges.pendingBanner !== undefined
|
||||
? pendingChanges.pendingBanner
|
||||
: userProfile?.banner;
|
||||
const avatarInput: ImageInput = hasImageInput(avatarToUse)
|
||||
? avatarToUse
|
||||
: IconUtils.getUserAvatarURL(currentUser, true, 512);
|
||||
const avatarDataUrl = await processImage(avatarInput, currentUser.id, "avatar", effectiveGuildId, useGuildAvatar);
|
||||
const resolvedAvatarDataUrl = avatarDataUrl ?? IconUtils.getDefaultAvatarURL(currentUser.id);
|
||||
|
||||
const bannerDataUrl = await processImage(bannerToUse, currentUser.id, "banner");
|
||||
const pendingBanner = pendingChanges.pendingBanner ?? pendingChanges.banner;
|
||||
const bannerToUse: ImageInput = hasImageInput(pendingBanner)
|
||||
? pendingBanner
|
||||
: (isGuildProfile ? (guildProfile?.banner ?? baseProfile?.banner) : baseProfile?.banner);
|
||||
const useGuildBanner = !!(effectiveGuildId && isGuildProfile && guildProfile?.banner && bannerToUse === guildProfile?.banner);
|
||||
|
||||
const bannerDataUrl = await processImage(bannerToUse, currentUser.id, "banner", effectiveGuildId, useGuildBanner);
|
||||
|
||||
return {
|
||||
avatarDataUrl,
|
||||
avatarDataUrl: resolvedAvatarDataUrl,
|
||||
bannerDataUrl,
|
||||
bio: pendingChanges.pendingBio !== undefined ? pendingChanges.pendingBio : userProfile?.bio || null,
|
||||
accentColor: pendingChanges.pendingAccentColor !== undefined ? pendingChanges.pendingAccentColor : userProfile?.accentColor || null,
|
||||
themeColors: pendingChanges.pendingThemeColors !== undefined
|
||||
? pendingChanges.pendingThemeColors
|
||||
: userProfile?.themeColors || null,
|
||||
globalName: pendingChanges.pendingGlobalName !== undefined ? pendingChanges.pendingGlobalName : currentUser.globalName || null,
|
||||
pronouns: pendingChanges.pendingPronouns !== undefined ? pendingChanges.pendingPronouns : userProfile?.pronouns || null,
|
||||
bio: pendingChanges.pendingBio ?? userProfile?.bio ?? null,
|
||||
accentColor: pendingChanges.pendingAccentColor ?? userProfile?.accentColor ?? null,
|
||||
themeColors: pendingChanges.pendingThemeColors ?? userProfile?.themeColors ?? null,
|
||||
globalName: isGuildProfile
|
||||
? (pendingChanges.pendingNickname ?? guildMember?.nick ?? null)
|
||||
: (pendingChanges.pendingGlobalName ?? currentUser.globalName ?? null),
|
||||
pronouns: pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null,
|
||||
avatarDecoration,
|
||||
profileEffect,
|
||||
nameplate,
|
||||
primaryGuildId: pendingChanges.pendingPrimaryGuildId !== undefined ? pendingChanges.pendingPrimaryGuildId : userAny.primaryGuild?.identityGuildId || null,
|
||||
primaryGuildId: isGuildProfile
|
||||
? null
|
||||
: (pendingChanges.pendingPrimaryGuildId ?? userAny.primaryGuild?.identityGuildId ?? null),
|
||||
customStatus,
|
||||
displayNameStyles
|
||||
};
|
||||
}
|
||||
|
||||
function jsonEq(a: any, b: any): boolean {
|
||||
function jsonEq(a: unknown, b: unknown): boolean {
|
||||
if (a === b) return true;
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
|
@ -182,158 +271,93 @@ function customStatusEq(a: CustomStatus | null | undefined, b: CustomStatus | nu
|
|||
&& String(a.expiresAtMs ?? "0") === String(b.expiresAtMs ?? "0");
|
||||
}
|
||||
|
||||
export async function loadPresetAsPending(preset: ProfilePreset, guildId?: string) {
|
||||
export async function loadPresetAsPending(preset: ProfilePreset, guildId?: string, options: LoadPresetOptions = {}) {
|
||||
try {
|
||||
const current = await getCurrentProfile(guildId);
|
||||
const isGuild = options.isGuildProfile ?? Boolean(guildId);
|
||||
if (isGuild && !guildId) return;
|
||||
const current = await getCurrentProfile(guildId, {
|
||||
isGuildProfile: isGuild
|
||||
});
|
||||
const profilePrefix = "USER_SETTINGS_ACCOUNT";
|
||||
const dispatchProfile = (type: string, payload: Record<string, unknown>) => {
|
||||
dispatch(type, isGuild ? { ...payload, guildId } : payload);
|
||||
};
|
||||
const setPending = (type: string, payload: Record<string, unknown>) => dispatchProfile(type, payload);
|
||||
|
||||
if (preset.avatarDataUrl && preset.avatarDataUrl !== current.avatarDataUrl) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR", { guildId, avatar: preset.avatarDataUrl });
|
||||
if ("avatarDataUrl" in preset) {
|
||||
const avatarValue = preset.avatarDataUrl;
|
||||
if (isGuild || avatarValue !== current.avatarDataUrl) {
|
||||
setPending(`${profilePrefix}_SET_PENDING_AVATAR`, { avatar: avatarValue });
|
||||
}
|
||||
}
|
||||
|
||||
if (preset?.bannerDataUrl && preset.bannerDataUrl !== current.bannerDataUrl) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER", { guildId, banner: preset.bannerDataUrl });
|
||||
if ("bannerDataUrl" in preset && preset.bannerDataUrl !== current.bannerDataUrl) {
|
||||
setPending(`${profilePrefix}_SET_PENDING_BANNER`, { banner: preset.bannerDataUrl });
|
||||
}
|
||||
|
||||
if (preset?.bio && preset.bio !== current.bio) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_BIO", { guildId, bio: preset.bio });
|
||||
if (!options.skipBio && "bio" in preset && preset.bio !== current.bio) {
|
||||
const bioValue = preset.bio === null ? "" : preset.bio;
|
||||
setPending(`${profilePrefix}_SET_PENDING_BIO`, { bio: bioValue });
|
||||
}
|
||||
|
||||
if (preset?.pronouns && preset.pronouns !== current.pronouns) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS", { guildId, pronouns: preset.pronouns });
|
||||
if (!options.skipPronouns && "pronouns" in preset && preset.pronouns !== current.pronouns) {
|
||||
const pronounsValue = preset.pronouns === null ? "" : preset.pronouns;
|
||||
setPending(`${profilePrefix}_SET_PENDING_PRONOUNS`, { pronouns: pronounsValue });
|
||||
}
|
||||
|
||||
if (preset?.globalName && preset.globalName !== current.globalName) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", { globalName: preset.globalName });
|
||||
if (!options.skipGlobalName && preset.globalName !== undefined && preset.globalName !== current.globalName) {
|
||||
if (isGuild) {
|
||||
setPending("USER_SETTINGS_ACCOUNT_SET_PENDING_NICKNAME", { nickname: preset.globalName });
|
||||
} else {
|
||||
setPending("USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", { globalName: preset.globalName });
|
||||
}
|
||||
}
|
||||
|
||||
if (preset?.avatarDecoration && !jsonEq(preset.avatarDecoration, current.avatarDecoration)) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_COLLECTIBLES_ITEM", {
|
||||
guildId,
|
||||
if (preset.avatarDecoration !== undefined && !jsonEq(preset.avatarDecoration, current.avatarDecoration)) {
|
||||
dispatchProfile(`${profilePrefix}_SET_PENDING_COLLECTIBLES_ITEM`, {
|
||||
item: { type: 0, value: preset.avatarDecoration }
|
||||
});
|
||||
}
|
||||
|
||||
if (preset?.profileEffect && !jsonEq(preset.profileEffect, current.profileEffect)) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_COLLECTIBLES_ITEM", {
|
||||
guildId,
|
||||
if (preset.profileEffect !== undefined && !jsonEq(preset.profileEffect, current.profileEffect)) {
|
||||
dispatchProfile(`${profilePrefix}_SET_PENDING_COLLECTIBLES_ITEM`, {
|
||||
item: { type: 1, value: preset.profileEffect }
|
||||
});
|
||||
}
|
||||
|
||||
if (preset?.nameplate && !jsonEq(preset.nameplate, current.nameplate)) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_COLLECTIBLES_ITEM", {
|
||||
guildId,
|
||||
if (preset.nameplate !== undefined && !jsonEq(preset.nameplate, current.nameplate)) {
|
||||
dispatchProfile(`${profilePrefix}_SET_PENDING_COLLECTIBLES_ITEM`, {
|
||||
item: { type: 2, value: preset.nameplate }
|
||||
});
|
||||
}
|
||||
|
||||
if (preset?.displayNameStyles && !jsonEq(preset.displayNameStyles, current.displayNameStyles)) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_DISPLAY_NAME_STYLES", {
|
||||
guildId,
|
||||
displayNameStyles: preset.displayNameStyles
|
||||
});
|
||||
if (preset.displayNameStyles !== undefined && !jsonEq(preset.displayNameStyles, current.displayNameStyles)) {
|
||||
setPending(`${profilePrefix}_SET_PENDING_DISPLAY_NAME_STYLES`, { displayNameStyles: preset.displayNameStyles });
|
||||
}
|
||||
|
||||
if (preset?.primaryGuildId && !guildId && preset.primaryGuildId !== current.primaryGuildId) {
|
||||
if (preset.themeColors !== undefined && !jsonEq(preset.themeColors, current.themeColors)) {
|
||||
setPending(`${profilePrefix}_SET_PENDING_THEME_COLORS`, { themeColors: preset.themeColors });
|
||||
}
|
||||
|
||||
if (preset.primaryGuildId !== undefined && !isGuild && preset.primaryGuildId !== current.primaryGuildId) {
|
||||
dispatch("USER_SETTINGS_SET_PENDING_PRIMARY_GUILD_ID", { primaryGuildId: preset.primaryGuildId });
|
||||
}
|
||||
|
||||
if (preset.customStatus && !guildId && !customStatusEq(preset.customStatus, current.customStatus)) {
|
||||
if (preset.customStatus !== undefined && !isGuild && !customStatusEq(preset.customStatus, current.customStatus)) {
|
||||
const customStatusValue = preset.customStatus ?? {
|
||||
text: "",
|
||||
expiresAtMs: "0",
|
||||
emojiId: "0",
|
||||
emojiName: ""
|
||||
};
|
||||
CustomStatusSettings.updateSetting({
|
||||
text: preset.customStatus.text || "",
|
||||
expiresAtMs: preset.customStatus.expiresAtMs || "0",
|
||||
emojiId: preset.customStatus.emojiId || "0",
|
||||
emojiName: preset.customStatus.emojiName || ""
|
||||
text: customStatusValue.text || "",
|
||||
expiresAtMs: customStatusValue.expiresAtMs || "0",
|
||||
emojiId: customStatusValue.emojiId || "0",
|
||||
emojiName: customStatusValue.emojiName || ""
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to load profile", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyUserProfile(userId: string) {
|
||||
const user = UserStore.getUser(userId);
|
||||
const userProfile = UserProfileStore.getUserProfile(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
if (userProfile?.banner) {
|
||||
const bannerUrl = `https://cdn.discordapp.com/banners/${userId}/${userProfile.banner}.${userProfile.banner.startsWith("a_") ? "gif" : "png"}?size=1024`;
|
||||
const bannerDataUrl = await imageUrlToBase64(bannerUrl);
|
||||
if (bannerDataUrl) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER", { guildId: undefined, banner: bannerDataUrl });
|
||||
}
|
||||
}
|
||||
|
||||
if (userProfile?.bio) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_BIO", { guildId: undefined, bio: userProfile.bio });
|
||||
}
|
||||
|
||||
if (userProfile?.pronouns) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS", { guildId: undefined, pronouns: userProfile.pronouns });
|
||||
}
|
||||
|
||||
if (userProfile?.themeColors) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_THEME_COLORS", { guildId: undefined, themeColors: userProfile.themeColors });
|
||||
}
|
||||
|
||||
const userAny = user as any;
|
||||
if (userAny.avatarDecorationData) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_COLLECTIBLES_ITEM", {
|
||||
guildId: undefined,
|
||||
item: {
|
||||
type: 0,
|
||||
value: {
|
||||
asset: userAny.avatarDecorationData.asset,
|
||||
skuId: userAny.avatarDecorationData.skuId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (userAny.collectibles?.nameplate) {
|
||||
const np = userAny.collectibles.nameplate;
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_COLLECTIBLES_ITEM", {
|
||||
guildId: undefined,
|
||||
item: {
|
||||
type: 2,
|
||||
value: {
|
||||
skuId: np.skuId,
|
||||
asset: np.asset,
|
||||
label: np.label,
|
||||
palette: np.palette,
|
||||
type: 2
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (userProfile?.profileEffect?.skuId) {
|
||||
const collectible = userProfile.collectibles?.find((c: any) => c.skuId === userProfile.profileEffect.skuId);
|
||||
if (collectible) {
|
||||
dispatch("USER_SETTINGS_ACCOUNT_SET_PENDING_COLLECTIBLES_ITEM", {
|
||||
guildId: undefined,
|
||||
item: {
|
||||
type: 1,
|
||||
value: {
|
||||
skuId: collectible.skuId,
|
||||
title: collectible.title,
|
||||
description: collectible.description,
|
||||
accessibilityLabel: collectible.accessibilityLabel,
|
||||
reducedMotionSrc: collectible.reducedMotionSrc,
|
||||
thumbnailPreviewSrc: collectible.thumbnailPreviewSrc,
|
||||
effects: collectible.effects,
|
||||
animationType: collectible.animationType,
|
||||
staticFrameSrc: collectible.staticFrameSrc,
|
||||
type: collectible.type || 1
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return user.username;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,28 +7,78 @@
|
|||
import { DataStore } from "@api/index";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { ProfilePreset } from "@vencord/discord-types";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
const logger = new Logger("ProfilePresets");
|
||||
const PRESETS_KEY = "ProfileDataset";
|
||||
const LEGACY_PRESETS_KEY = "ProfileDataset";
|
||||
const MAIN_PRESETS_KEY = "ProfilePresets_v2_Main";
|
||||
const SERVER_PRESETS_KEY = "ProfilePresets_v2_Server";
|
||||
|
||||
export let presets: ProfilePreset[] = [];
|
||||
export type PresetSection = "main" | "server";
|
||||
|
||||
export type ProfilePresetEx = ProfilePreset & {
|
||||
avatarRaw?: string | null;
|
||||
};
|
||||
|
||||
export let presets: ProfilePresetEx[] = [];
|
||||
export let currentPresetIndex = -1;
|
||||
let activeScopeKey: string | null = null;
|
||||
|
||||
export async function loadPresets() {
|
||||
function resetPresets(nextPresets: ProfilePresetEx[] = []) {
|
||||
presets = nextPresets;
|
||||
currentPresetIndex = -1;
|
||||
}
|
||||
|
||||
function getPresetsKey(section: PresetSection, userId: string) {
|
||||
const baseKey = section === "main" ? MAIN_PRESETS_KEY : SERVER_PRESETS_KEY;
|
||||
return `${baseKey}:${userId}`;
|
||||
}
|
||||
|
||||
function getLegacyKey(userId: string) {
|
||||
return `${LEGACY_PRESETS_KEY}:${userId}:main`;
|
||||
}
|
||||
|
||||
export async function loadPresets(section: PresetSection) {
|
||||
try {
|
||||
const stored = await DataStore.get(PRESETS_KEY);
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const userId = currentUser!.id;
|
||||
const key = getPresetsKey(section, userId);
|
||||
activeScopeKey = key;
|
||||
const stored = await DataStore.get(key);
|
||||
if (stored && Array.isArray(stored)) {
|
||||
presets = stored;
|
||||
resetPresets(stored);
|
||||
return;
|
||||
}
|
||||
|
||||
if (section === "main") {
|
||||
const legacyKey = getLegacyKey(userId);
|
||||
const legacyStored = await DataStore.get(legacyKey);
|
||||
const legacyBaseStored = await DataStore.get(LEGACY_PRESETS_KEY);
|
||||
const legacyToUse = Array.isArray(legacyStored)
|
||||
? legacyStored
|
||||
: (Array.isArray(legacyBaseStored) ? legacyBaseStored : null);
|
||||
if (legacyToUse) {
|
||||
resetPresets(legacyToUse);
|
||||
await DataStore.set(key, legacyToUse);
|
||||
await DataStore.del(legacyKey);
|
||||
await DataStore.del(LEGACY_PRESETS_KEY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
resetPresets();
|
||||
} catch (err) {
|
||||
logger.error("Failed to load presets", err);
|
||||
presets = [];
|
||||
resetPresets();
|
||||
}
|
||||
}
|
||||
|
||||
export async function savePresetsData() {
|
||||
export async function savePresetsData(section?: PresetSection) {
|
||||
try {
|
||||
await DataStore.set(PRESETS_KEY, presets);
|
||||
if (!activeScopeKey && !section) return;
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const userId = currentUser!.id;
|
||||
const key = section ? getPresetsKey(section, userId) : activeScopeKey!;
|
||||
await DataStore.set(key, presets);
|
||||
} catch (err) {
|
||||
logger.error("Failed to save presets", err);
|
||||
}
|
||||
|
|
@ -38,11 +88,11 @@ export function setCurrentPresetIndex(index: number) {
|
|||
currentPresetIndex = index;
|
||||
}
|
||||
|
||||
export function addPreset(preset: ProfilePreset) {
|
||||
export function addPreset(preset: ProfilePresetEx) {
|
||||
presets.push(preset);
|
||||
}
|
||||
|
||||
export function updatePreset(index: number, preset: ProfilePreset) {
|
||||
export function updatePreset(index: number, preset: ProfilePresetEx) {
|
||||
if (index >= 0 && index < presets.length) {
|
||||
presets[index] = preset;
|
||||
}
|
||||
|
|
@ -65,6 +115,6 @@ export function movePresetInArray(fromIndex: number, toIndex: number) {
|
|||
presets.splice(toIndex, 0, preset);
|
||||
}
|
||||
|
||||
export function replaceAllPresets(newPresets: ProfilePreset[]) {
|
||||
export function replaceAllPresets(newPresets: ProfilePresetEx[]) {
|
||||
presets = newPresets;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue