diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 7faf34756..65b2ac1e0 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -713,3 +713,19 @@ export function CloudUploadIcon(props: IconProps) { ); } + +export const ClockIcon = (props?: any) => { + return ( + + + + ); +}; diff --git a/src/equicordplugins/components.dev/index.tsx b/src/equicordplugins/components.dev/index.tsx index 5e97d073c..68f05fb63 100644 --- a/src/equicordplugins/components.dev/index.tsx +++ b/src/equicordplugins/components.dev/index.tsx @@ -6,8 +6,9 @@ import "./styles.css"; -import SettingsPlugin, { settingsSectionMap } from "@plugins/_core/settings"; +import SettingsPlugin from "@plugins/_core/settings"; import { Devs } from "@utils/constants"; +import { removeFromArray } from "@utils/misc"; import definePlugin, { StartAt } from "@utils/types"; import { SettingsRouter } from "@webpack/common"; @@ -49,32 +50,17 @@ export default definePlugin({ }, }, start() { - const { customEntries, customSections } = SettingsPlugin; - - customEntries.push({ + SettingsPlugin.customEntries.push({ key: "equicord_components", title: "Components", Component: ComponentsTab, Icon: ComponentsIcon }); - customSections.push(() => ({ - section: "EquicordDiscordComponents", - label: "Components", - element: ComponentsTab, - className: "vc-discord-components", - id: "EquicordDiscordComponents" - })); - - settingsSectionMap.push(["EquicordDiscordComponents", "equicord_components"]); + SettingsPlugin.settingsSectionMap.push(["EquicordDiscordComponents", "equicord_components"]); }, stop() { - const { customEntries, customSections } = SettingsPlugin; - const entryIdx = customEntries.findIndex(e => e.key === "equicord_components"); - if (entryIdx !== -1) customEntries.splice(entryIdx, 1); - const sectionIdx = customSections.findIndex(s => s({} as any).id === "EquicordDiscordComponents"); - if (sectionIdx !== -1) customSections.splice(sectionIdx, 1); - const map = settingsSectionMap.findIndex(entry => entry[1] === "equicord_components"); - if (map !== -1) settingsSectionMap.splice(map, 1); + removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_components"); + removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_components"); }, }); diff --git a/src/equicordplugins/iRememberYou/index.tsx b/src/equicordplugins/iRememberYou/index.tsx index e2056414b..8b4614331 100644 --- a/src/equicordplugins/iRememberYou/index.tsx +++ b/src/equicordplugins/iRememberYou/index.tsx @@ -8,8 +8,9 @@ import "./styles.css"; import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { EyeIcon } from "@components/Icons"; -import SettingsPlugin, { settingsSectionMap } from "@plugins/_core/settings"; +import SettingsPlugin from "@plugins/_core/settings"; import { EquicordDevs } from "@utils/constants"; +import { removeFromArray } from "@utils/misc"; import definePlugin from "@utils/types"; import { Data } from "./components/data"; @@ -24,23 +25,13 @@ export default definePlugin({ patches: [], async start() { - const { customEntries, customSections } = SettingsPlugin; - - customEntries.push({ + SettingsPlugin.customEntries.push({ key: "equicord_i_remember_you", title: "I Remember You", Component: () => , Icon: EyeIcon }); - - customSections.push(() => ({ - section: "EquicordIRememberYou", - label: "IRememberYou", - element: () => , - id: "EquicordIRememberYou" - })); - - settingsSectionMap.push(["EquicordIRememberYou", "equicord_i_remember_you"]); + SettingsPlugin.settingsSectionMap.push(["EquicordIRememberYou", "equicord_i_remember_you"]); const data = (this.dataManager = await new Data().withStart()); @@ -54,15 +45,10 @@ export default definePlugin({ }, stop() { - const dataManager = this.dataManager as Data; - const { customEntries, customSections } = SettingsPlugin; - const entry = customEntries.findIndex(entry => entry.key === "equicord_i_remember_you"); - if (entry !== -1) customEntries.splice(entry, 1); - const section = customSections.findIndex(section => section({} as any).id === "EquicordIRememberYou"); - if (section !== -1) customSections.splice(section, 1); - const map = settingsSectionMap.findIndex(entry => entry[1] === "equicord_i_remember_you"); - if (map !== -1) settingsSectionMap.splice(map, 1); + removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_i_remember_you"); + removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_i_remember_you"); + const dataManager = this.dataManager as Data; removeMessagePreSendListener(dataManager._onMessagePreSend_preSend); clearInterval(dataManager._storageAutoSaveProtocol_interval); }, diff --git a/src/equicordplugins/iconViewer/index.tsx b/src/equicordplugins/iconViewer/index.tsx index 5c4871526..f2135cbf2 100644 --- a/src/equicordplugins/iconViewer/index.tsx +++ b/src/equicordplugins/iconViewer/index.tsx @@ -7,8 +7,9 @@ import "./styles.css"; import { MagnifyingGlassIcon } from "@components/Icons"; -import SettingsPlugin, { settingsSectionMap } from "@plugins/_core/settings"; +import SettingsPlugin from "@plugins/_core/settings"; import { EquicordDevs } from "@utils/constants"; +import { removeFromArray } from "@utils/misc"; import definePlugin, { StartAt } from "@utils/types"; import { SettingsRouter } from "@webpack/common"; @@ -28,32 +29,17 @@ export default definePlugin({ }, settingsAboutComponent: SettingsAbout, start() { - const { customEntries, customSections } = SettingsPlugin; - - customEntries.push({ + SettingsPlugin.customEntries.push({ key: "equicord_icon_viewer", title: "Icon Finder", Component: IconsTab, Icon: MagnifyingGlassIcon }); - customSections.push(() => ({ - section: "EquicordDiscordIcons", - label: "Icon Finder", - element: IconsTab, - className: "vc-discord-icons", - id: "EquicordIconViewer" - })); - - settingsSectionMap.push(["EquicordDiscordIcons", "equicord_icon_viewer"]); + SettingsPlugin.settingsSectionMap.push(["EquicordDiscordIcons", "equicord_icon_viewer"]); }, stop() { - const { customEntries, customSections } = SettingsPlugin; - const entryIdx = customEntries.findIndex(e => e.key === "equicord_icon_viewer"); - if (entryIdx !== -1) customEntries.splice(entryIdx, 1); - const sectionIdx = customSections.findIndex(s => s({} as any).id === "EquicordIconViewer"); - if (sectionIdx !== -1) customSections.splice(sectionIdx, 1); - const map = settingsSectionMap.findIndex(entry => entry[1] === "equicord_icon_viewer"); - if (map !== -1) settingsSectionMap.splice(map, 1); + removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_icon_viewer"); + removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_icon_viewer"); }, }); diff --git a/src/equicordplugins/loginWithQR/index.tsx b/src/equicordplugins/loginWithQR/index.tsx index be35d1f87..54d15387b 100644 --- a/src/equicordplugins/loginWithQR/index.tsx +++ b/src/equicordplugins/loginWithQR/index.tsx @@ -7,9 +7,10 @@ import { plugins } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import { Paragraph } from "@components/Paragraph"; -import SettingsPlugin, { settingsSectionMap } from "@plugins/_core/settings"; +import SettingsPlugin from "@plugins/_core/settings"; import { EquicordDevs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; +import { removeFromArray } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { Button } from "@webpack/common"; @@ -59,37 +60,19 @@ export default definePlugin({ qrModalOpen: false, start() { - const { customEntries, customSections } = SettingsPlugin; - - customEntries.push({ + SettingsPlugin.customEntries.push({ key: "equicord_login_with_qr", title: getIntlMessage("USER_SETTINGS_SCAN_QR_CODE"), Component: openQrModal, Icon: QrCodeIcon }); - - customSections.push(() => ({ - section: "EquicordLoginWithQR", - label: getIntlMessage("USER_SETTINGS_SCAN_QR_CODE"), - searchableTitles: [getIntlMessage("USER_SETTINGS_SCAN_QR_CODE")], - element: openQrModal, - id: "EquicordLoginWithQR", - })); - - settingsSectionMap.push(["EquicordLoginWithQR", "equicord_login_with_qr"]); - + SettingsPlugin.settingsSectionMap.push(["EquicordLoginWithQR", "equicord_login_with_qr"]); preload(); }, stop() { - const { customEntries, customSections } = SettingsPlugin; - const entry = customEntries.findIndex(entry => entry.key === "equicord_login_with_qr"); - if (entry !== -1) customEntries.splice(entry, 1); - const section = customSections.findIndex(section => section({} as any).id === "EquicordLoginWithQR"); - if (section !== -1) customSections.splice(section, 1); - const map = settingsSectionMap.findIndex(entry => entry[1] === "equicord_login_with_qr"); - if (map !== -1) settingsSectionMap.splice(map, 1); - + removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_login_with_qr"); + removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_login_with_qr"); unload(); }, }); diff --git a/src/equicordplugins/profileSets/components/confirmModal.tsx b/src/equicordplugins/profileSets/components/confirmModal.tsx index 2b60da113..3dfa02f1e 100644 --- a/src/equicordplugins/profileSets/components/confirmModal.tsx +++ b/src/equicordplugins/profileSets/components/confirmModal.tsx @@ -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 ( @@ -27,24 +40,42 @@ export function ConfirmModal({ title, message, confirmText, cancelText, onConfir {message} - - ); } + +export function ImportProfilesModal({ title, message, onOverride, onMerge, onCancel, onClose, transitionState }: ImportProfilesModalProps) { + const closeAfter = (action: () => void) => () => { + action(); + onClose(); + }; + + return ( + + + {title} + + + {message} + + + + + + + + ); +} diff --git a/src/equicordplugins/profileSets/components/presetList.tsx b/src/equicordplugins/profileSets/components/presetList.tsx index e5e8c011c..2beb5bc9e 100644 --- a/src/equicordplugins/profileSets/components/presetList.tsx +++ b/src/equicordplugins/profileSets/components/presetList.tsx @@ -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(-1); const [renameText, setRenameText] = React.useState(""); + const isGuildProfile = section === "server"; return (
{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 (
{ 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; } diff --git a/src/equicordplugins/themeLibrary/index.tsx b/src/equicordplugins/themeLibrary/index.tsx index c561ba04c..befdadca0 100644 --- a/src/equicordplugins/themeLibrary/index.tsx +++ b/src/equicordplugins/themeLibrary/index.tsx @@ -5,8 +5,9 @@ */ import { ColorPaletteIcon } from "@components/Icons"; -import SettingsPlugin, { settingsSectionMap } from "@plugins/_core/settings"; +import SettingsPlugin from "@plugins/_core/settings"; import { EquicordDevs } from "@utils/constants"; +import { removeFromArray } from "@utils/misc"; import definePlugin from "@utils/types"; import { SettingsRouter } from "@webpack/common"; @@ -24,33 +25,18 @@ export default definePlugin({ }, start() { - const { customEntries, customSections } = SettingsPlugin; - - customEntries.push({ + SettingsPlugin.customEntries.push({ key: "theme_library", title: "Theme Library", Component: require("./components/ThemeTab").default, Icon: ColorPaletteIcon }); - customSections.push(() => ({ - section: "EquicordThemeLibrary", - label: "Theme Library", - searchableTitles: ["Theme Library"], - element: require("./components/ThemeTab").default, - id: "EquicordThemeLibrary", - })); - - settingsSectionMap.push(["EquicordThemeLibrary", "equicord_theme_library"]); + SettingsPlugin.settingsSectionMap.push(["EquicordThemeLibrary", "equicord_theme_library"]); }, stop() { - const { customEntries, customSections } = SettingsPlugin; - const entry = customEntries.findIndex(entry => entry.key === "equicord_theme_library"); - if (entry !== -1) customEntries.splice(entry, 1); - const section = customSections.findIndex(section => section({} as any).id === "EquicordThemeLibrary"); - if (section !== -1) customSections.splice(section, 1); - const map = settingsSectionMap.findIndex(entry => entry[1] === "equicord_theme_library"); - if (map !== -1) settingsSectionMap.splice(map, 1); + removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_theme_library"); + removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_theme_library"); }, }); diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index bc675c55a..d5a57d933 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -18,7 +18,6 @@ import { } from "@components/settings"; import { gitHashShort } from "@shared/vencordUserAgent"; import { Devs } from "@utils/constants"; -import { getIntlMessage } from "@utils/discord"; import { isTruthy } from "@utils/guards"; import definePlugin, { IconProps, OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; @@ -104,7 +103,7 @@ const settings = definePluginSettings({ } }); -export const settingsSectionMap: [string, string][] = [ +const settingsSectionMap: [string, string][] = [ ["EquicordSettings", "equicord_main_panel"], ["EquicordPlugins", "equicord_plugins_panel"], ["EquicordThemes", "equicord_themes_panel"], @@ -123,6 +122,7 @@ export default definePlugin({ required: true, settings, + settingsSectionMap, patches: [ { @@ -301,126 +301,6 @@ export default definePlugin({ customSections: [] as ((SectionTypes: Record) => { section: string; element: ComponentType; label: string; id?: string; })[], customEntries: [] as EntryOptions[], - makeSettingsCategories(SectionTypes: Record) { - return [ - { - section: SectionTypes.HEADER, - label: "Equicord", - className: "vc-settings-header", - }, - { - section: "EquicordSettings", - label: "Equicord", - element: VencordTab, - className: "vc-settings", - }, - { - section: "EquicordPlugins", - label: "Plugins", - searchableTitles: ["Plugins"], - element: PluginsTab, - className: "vc-plugins", - }, - { - section: "EquicordThemes", - label: "Themes", - searchableTitles: ["Themes"], - element: ThemesTab, - className: "vc-themes", - }, - !IS_UPDATER_DISABLED && { - section: "EquicordUpdater", - label: "Updater", - searchableTitles: ["Updater"], - element: UpdaterTab, - className: "vc-updater", - }, - { - section: "EquicordChangelog", - label: "Changelog", - searchableTitles: ["Changelog"], - element: ChangelogTab, - className: "vc-changelog", - }, - { - section: "EquicordCloud", - label: "Cloud", - searchableTitles: ["Cloud"], - element: CloudTab, - className: "vc-cloud", - }, - { - section: "EquicordBackupAndRestore", - label: "Backup & Restore", - searchableTitles: ["Backup & Restore"], - element: BackupAndRestoreTab, - className: "vc-backup-restore", - }, - IS_DEV && { - section: "EquicordPatchHelper", - label: "Patch Helper", - searchableTitles: ["Patch Helper"], - element: PatchHelperTab, - className: "vc-patch-helper", - }, - ...this.customSections.map(func => func(SectionTypes)), - { - section: SectionTypes.DIVIDER, - }, - ].filter(Boolean); - }, - - isRightSpot({ header, settings: s }: { header?: string; settings?: string[]; }) { - const firstChild = s?.[0]; - if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true; - - const { settingsLocation } = settings.store; - - if (settingsLocation === "bottom") return firstChild === "LOGOUT"; - if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG"; - - if (!header) return; - - try { - const names: Record, string> = { - top: getIntlMessage("USER_SETTINGS"), - aboveNitro: getIntlMessage("BILLING_SETTINGS"), - belowNitro: getIntlMessage("APP_SETTINGS"), - aboveActivity: getIntlMessage("ACTIVITY_SETTINGS"), - }; - - if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS")) - return firstChild === "PREMIUM"; - - return header === names[settingsLocation]; - } catch { - return firstChild === "PREMIUM"; - } - }, - - patchedSettings: new WeakSet(), - - addSettings( - elements: any[], - element: { header?: string; settings: string[]; }, - SectionTypes: Record, - ) { - if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return; - - this.patchedSettings.add(elements); - elements.push(...this.makeSettingsCategories(SectionTypes)); - }, - - wrapSettingsHook(originalHook: (...args: any[]) => Record[]) { - return (...args: any[]) => { - const elements = originalHook(...args); - if (!this.patchedSettings.has(elements)) - elements.unshift(...this.makeSettingsCategories({ HEADER: SectionType.HEADER, DIVIDER: SectionType.DIVIDER, CUSTOM: SectionType.CUSTOM }) as Record[]); - - return elements; - }; - }, - get electronVersion() { return VencordNative.native.getVersions().electron ?? window.legcord?.electron ?? null; }, diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index e2783e92d..605d3055d 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -102,12 +102,10 @@ export default definePlugin({ "#{intl::ayozFl::raw}", // Avatar preview ].map(find => ({ find, - replacement: [ - { - match: /(?<=userValue:)((\i(?:\.author)?)\?\.avatarDecoration)/, - replace: "$self.useUserDecorAvatarDecoration($2)??$1" - } - ] + replacement: { + match: /(?<=userValue:)((\i(?:\.author)?)\?\.avatarDecoration)/, + replace: "$self.useUserDecorAvatarDecoration($2)??$1" + } })), // Patch avatar decoration preview to display Decor avatar decorations as if they are purchased { diff --git a/src/plugins/startupTimings/index.tsx b/src/plugins/startupTimings/index.tsx index a21dddf27..3b92c5f04 100644 --- a/src/plugins/startupTimings/index.tsx +++ b/src/plugins/startupTimings/index.tsx @@ -16,9 +16,10 @@ * along with this program. If not, see . */ -import { ClockIcon } from "@equicordplugins/themeLibrary/utils/Icons"; -import SettingsPlugin, { settingsSectionMap } from "@plugins/_core/settings"; +import { ClockIcon } from "@components/Icons"; +import SettingsPlugin from "@plugins/_core/settings"; import { Devs } from "@utils/constants"; +import { removeFromArray } from "@utils/misc"; import definePlugin from "@utils/types"; import StartupTimingPage from "./StartupTimingPage"; @@ -28,32 +29,16 @@ export default definePlugin({ description: "Adds Startup Timings to the Settings menu", authors: [Devs.Megu], start() { - const { customEntries, customSections } = SettingsPlugin; - - customEntries.push({ - key: "equicord_startup_timings", + SettingsPlugin.customEntries.push({ + key: "vencord_startup_timings", title: "Startup Timings", Component: StartupTimingPage, Icon: ClockIcon }); - - customSections.push(() => ({ - section: "EquicordStartupTimings", - label: "Startup Timings", - searchableTitles: ["Startup Timings"], - element: StartupTimingPage, - id: "EquicordStartupTimings", - })); - - settingsSectionMap.push(["EquicordStartupTimings", "equicord_startup_timings"]); + SettingsPlugin.settingsSectionMap.push(["VencordStartupTimings", "vencord_startup_timings"]); }, stop() { - const { customEntries, customSections } = SettingsPlugin; - const entryIdx = customEntries.findIndex(e => e.key === "equicord_startup_timings"); - if (entryIdx !== -1) customEntries.splice(entryIdx, 1); - const section = customSections.findIndex(section => section({} as any).id === "EquicordStartupTimings"); - if (section !== -1) customSections.splice(section, 1); - const map = settingsSectionMap.findIndex(entry => entry[1] === "equicord_startup_timings"); - if (map !== -1) customEntries.splice(map, 1); + removeFromArray(SettingsPlugin.customEntries, e => e.key === "vencord_startup_timings"); + removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "vencord_startup_timings"); }, }); diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 0c27b15da..28582ddd1 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -123,3 +123,8 @@ export function isEquicordSupport(userId: string | null | undefined): boolean { const member = GuildMemberStore.getMember(GUILD_ID, userId); return member?.roles?.includes(EQUICORD_HELPERS) || false; } + +export function removeFromArray(arr: T[], predicate: (e: T) => boolean) { + const idx = arr.findIndex(predicate); + if (idx !== -1) arr.splice(idx, 1); +} diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 0098b7253..fb31cbc23 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -611,18 +611,22 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory): markedAsPatched = true; } } catch (err) { - logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\n`, err); + // FIXME: Maybe fix this properly + const shouldSuppressError = patch.plugin === "ContextMenuAPI" && err instanceof SyntaxError && err.message.includes("arguments"); + if (!shouldSuppressError) { + logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\n`, err); - if (IS_COMPANION_TEST) - reporterData.failedPatches.erroredPatch.push({ - ...patch, - oldModule: lastCode, - newModule: code, - id: moduleId - }); + if (IS_COMPANION_TEST) + reporterData.failedPatches.erroredPatch.push({ + ...patch, + oldModule: lastCode, + newModule: code, + id: moduleId + }); - if (IS_DEV) { - diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!); + if (IS_DEV) { + diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!); + } } if (markedAsPatched) {