{
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" : "")}
>
{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();
}}
/>
diff --git a/src/equicordplugins/profileSets/components/presetManager.tsx b/src/equicordplugins/profileSets/components/presetManager.tsx
index 660abc687..1f07da8f5 100644
--- a/src/equicordplugins/profileSets/components/presetManager.tsx
+++ b/src/equicordplugins/profileSets/components/presetManager.tsx
@@ -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
(-1);
const [searchMode, setSearchMode] = React.useState(false);
+ const lastRandomIndexRef = React.useRef(-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 => {
+ 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 => {
return new Promise(resolve => {
openModal(props => (
- 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 (
-
+
Saved Profiles
@@ -99,7 +160,7 @@ export function PresetManager({ guildId }: { guildId?: string; }) {
{!searchMode && (
)}
-
-
-
-
-
>
)}
+
+
+
+
+
+
+
+ {resolvedSection === "server" && (
+
+ )}
);
}
diff --git a/src/equicordplugins/profileSets/index.tsx b/src/equicordplugins/profileSets/index.tsx
index 115cd0553..dccb74a8c 100644
--- a/src/equicordplugins/profileSets/index.tsx
+++ b/src/equicordplugins/profileSets/index.tsx
@@ -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 ;
+ renderPresetSection(section: PresetSection, guildId?: string) {
+ return ;
}
});
diff --git a/src/equicordplugins/profileSets/styles.css b/src/equicordplugins/profileSets/styles.css
index 432ca6dcc..390c827b9 100644
--- a/src/equicordplugins/profileSets/styles.css
+++ b/src/equicordplugins/profileSets/styles.css
@@ -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);
-}
\ No newline at end of file
+}
diff --git a/src/equicordplugins/profileSets/utils/actions.ts b/src/equicordplugins/profileSets/utils/actions.ts
index b225b48ea..eef4b916d 100644
--- a/src/equicordplugins/profileSets/utils/actions.ts
+++ b/src/equicordplugins/profileSets/utils/actions.ts
@@ -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, value: any) {
+export async function updatePresetField(index: number, field: keyof Omit, 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= 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) {
+export async function importPresets(
+ forceUpdate: () => void,
+ onImportPrompt: (existingCount: number) => Promise,
+ 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();
diff --git a/src/equicordplugins/profileSets/utils/profile.ts b/src/equicordplugins/profileSets/utils/profile.ts
index ecd9f6e54..2408623eb 100644
--- a/src/equicordplugins/profileSets/utils/profile.ts
+++ b/src/equicordplugins/profileSets/utils/profile.ts
@@ -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 & {
+ 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) {
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 {
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 {
+async function processImage(imageData: ImageInput, userId: string, type: "avatar" | "banner", guildId?: string, useGuildPath?: boolean): Promise {
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> {
- 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> {
+ 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 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) => {
+ dispatch(type, isGuild ? { ...payload, guildId } : payload);
+ };
+ const setPending = (type: string, payload: Record) => 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;
-}
diff --git a/src/equicordplugins/profileSets/utils/storage.ts b/src/equicordplugins/profileSets/utils/storage.ts
index f13d6865e..ff5cc965f 100644
--- a/src/equicordplugins/profileSets/utils/storage.ts
+++ b/src/equicordplugins/profileSets/utils/storage.ts
@@ -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;
}