Equicord/src/equicordplugins/profileSets/utils/actions.ts
Eazvy 189ab429bb
profileSets avatar / types fix (#794)
* profileSets avatar / types fix

- Fix avatar pending payload flow to use USER_PROFILE_SETTINGS_SET_PENDING_AVATAR with NEW_ASSET imageUri payload.

- Resolve TypeScript typing issues around IconUtils avatar/banner URL resolution without any casts.

- Remove debug logging and legacy pending dispatch paths; keep recents and profile pending behavior aligned with current Discord profile settings.

- Clean up profileSets rulebook compliance issues (nullish coalescing/defaults, typed preset field updates, no logger/console usage).

* profilesets: remove display name sanitizer subscription

* fixed checks

* fix pending profile event flow

* skip collectible dispatch when already equipped

* fixed checks

* Remove empty stop method from profile set plugin

Removed the empty stop method from the profile set plugin.

* bleh

* Update src/equicordplugins/profileSets/utils/profile.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update src/equicordplugins/profileSets/utils/profile.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* t

* fix fallback guard

* restore avatar size fallback

* fix fallback guard

* fix fallback guard

* fix to slider

* address gemini review issues

* fix avatarSize

* update avatarSize

* address review: destructure avatarSize

* restore direct avatarSize access

* destructure avatarSize

* Apply suggestion from @gemini-code-assist[bot]

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Apply suggestion from @gemini-code-assist[bot]

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Apply suggestion from @gemini-code-assist[bot]

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-20 16:06:46 -05:00

152 lines
5.4 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { isNonNullish } from "@utils/guards";
import { ProfilePreset } from "@vencord/discord-types";
import { findStoreLazy } from "@webpack";
import { showToast, Toasts } from "@webpack/common";
import { getCurrentProfile } from "./profile";
import { addPreset, movePresetInArray, presets, PresetSection, type ProfilePresetEx, removePreset, replaceAllPresets, savePresetsData, updatePreset } from "./storage";
const UserProfileSettingsStore = findStoreLazy("UserProfileSettingsStore");
function isImageInput(value: unknown): value is string | { imageUri: string; } {
if (typeof value === "string") return value.length > 0;
return typeof value === "object" && isNonNullish(value) && "imageUri" in value && typeof (value as { imageUri: unknown }).imageUri === "string";
}
function getFreshPendingAvatar(section: PresetSection, guildId?: string): string | null {
const pending = (section === "server" && guildId
? UserProfileSettingsStore.getPendingChanges?.(guildId)
: UserProfileSettingsStore.getPendingChanges?.()) ?? {};
const pendingObj = pending as Record<string, unknown>;
const candidates = [
pendingObj.selectedAvatarRaw,
pendingObj.presetAvatarRaw,
pendingObj.pendingAvatar,
pendingObj.avatar,
pendingObj.selectedAvatarProcessed,
pendingObj.presetAvatarProcessed
];
const selected = candidates.find(isImageInput);
if (!selected) return null;
return typeof selected === "string" ? selected : selected.imageUri;
}
export async function savePreset(name: string, section: PresetSection, guildId?: string) {
const profile = await getCurrentProfile(guildId, { isGuildProfile: section === "server" });
const freshPendingAvatar = getFreshPendingAvatar(section, guildId);
const effectiveAvatar = freshPendingAvatar ?? profile.avatarDataUrl ?? null;
const newPreset: ProfilePresetEx = {
name,
timestamp: Date.now(),
...profile,
avatarDataUrl: effectiveAvatar,
};
addPreset(newPreset);
await savePresetsData(section);
}
export async function updatePresetField<K extends keyof Omit<ProfilePreset, "name" | "timestamp">>(
index: number,
field: K,
value: Omit<ProfilePreset, "name" | "timestamp">[K],
section: PresetSection,
guildId?: string
) {
if (index < 0 || index >= presets.length) return;
void guildId;
const updatedPreset = {
...presets[index],
[field]: value,
timestamp: Date.now()
};
updatePreset(index, updatedPreset);
await savePresetsData(section);
}
export async function deletePreset(index: number, section: PresetSection, guildId?: string) {
if (index < 0 || index >= presets.length) return;
removePreset(index);
await savePresetsData(section);
}
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(section);
}
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(section);
}
export function exportPresets(section: PresetSection) {
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-${section}-${Date.now()}.json`;
link.click();
URL.revokeObjectURL(url);
}
export type ImportDecision = "override" | "merge" | "cancel";
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 (event: Event) => {
try {
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)) {
return;
}
if (presets.length > 0) {
const decision = await onImportPrompt(presets.length);
if (decision === "cancel") return;
if (decision === "override") {
replaceAllPresets(importedPresets);
} else {
const combined = [...presets, ...importedPresets];
replaceAllPresets(combined);
}
} else {
replaceAllPresets(importedPresets);
}
await savePresetsData(section);
forceUpdate();
} catch {
showToast("Failed to import presets. The file might be invalid.", Toasts.Type.FAILURE);
}
};
input.click();
}