From 01c1484c8de595ef5ef0a97753ce27d0a7c8b8f9 Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:20:50 +0100 Subject: [PATCH] add changelog section (#410) * add tone indicators plugin * im tired of this man * oh my fucking god its EquicordDevs not Devs * dont use json * Update index.tsx * Initial commit * past updates history, new plugins section, more git commit information. waho * move it to be under updater instead of cloud * LINT ERROR FIX PLEASE * wait why does it say 2022 * add support for new settings, history list scrollable and taller, data migration, debugging commands. * add support for required plugins, also name things logs instead of sessions, clean up code too * Fixes * holy changes. ability to bulk clear and specific clear logs with either a trash can icon or the "clear logs" button at the top of the page, plugin entries in the logs section showing if something is required of any other plugins, fetch logs from repository which checks for duplicate hash to prevent duplicate messy logs, fixed cramming of elements by extending the page itself, made things look better. * Fix Lint * Update styles.css * Remove NewPluginsManager * API Plugin Check --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: justjxke --- .../tabs/changelog/NewPluginsSection.tsx | 279 +++++++ .../tabs/changelog/changelogManager.ts | 282 +++++++ .../settings/tabs/changelog/index.tsx | 693 ++++++++++++++++++ .../settings/tabs/changelog/styles.css | 572 +++++++++++++++ src/components/settings/tabs/index.ts | 1 + .../newPluginsManager/NewPluginsModal.css | 20 - .../newPluginsManager/NewPluginsModal.tsx | 205 ------ .../newPluginsManager/index.tsx | 28 - .../newPluginsManager/knownSettings.ts | 91 --- src/plugins/_core/settings.tsx | 168 +++-- 10 files changed, 1933 insertions(+), 406 deletions(-) create mode 100644 src/components/settings/tabs/changelog/NewPluginsSection.tsx create mode 100644 src/components/settings/tabs/changelog/changelogManager.ts create mode 100644 src/components/settings/tabs/changelog/index.tsx create mode 100644 src/components/settings/tabs/changelog/styles.css delete mode 100644 src/equicordplugins/newPluginsManager/NewPluginsModal.css delete mode 100644 src/equicordplugins/newPluginsManager/NewPluginsModal.tsx delete mode 100644 src/equicordplugins/newPluginsManager/index.tsx delete mode 100644 src/equicordplugins/newPluginsManager/knownSettings.ts diff --git a/src/components/settings/tabs/changelog/NewPluginsSection.tsx b/src/components/settings/tabs/changelog/NewPluginsSection.tsx new file mode 100644 index 000000000..da1d07414 --- /dev/null +++ b/src/components/settings/tabs/changelog/NewPluginsSection.tsx @@ -0,0 +1,279 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { useSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import { PluginCard } from "@components/settings/tabs/plugins/PluginCard"; +import { ChangeList } from "@utils/ChangeList"; +import { Margins } from "@utils/margins"; +import { useForceUpdater } from "@utils/react"; +import { Button, Card, Forms, React, Tooltip } from "@webpack/common"; + +import Plugins from "~plugins"; + +const cl = classNameFactory("vc-changelog-"); + +interface NewPluginsSectionProps { + newPlugins: string[]; + onPluginToggle?: (pluginName: string, enabled: boolean) => void; +} + +export function NewPluginsSection({ + newPlugins, + onPluginToggle, +}: NewPluginsSectionProps) { + const settings = useSettings(); + const changes = React.useMemo(() => new ChangeList(), []); + const forceUpdate = useForceUpdater(); + + const depMap = React.useMemo(() => { + const o = {} as Record; + for (const plugin in Plugins) { + const deps = Plugins[plugin].dependencies; + if (deps) { + for (const dep of deps) { + o[dep] ??= []; + o[dep].push(plugin); + } + } + } + return o; + }, []); + + const mapPlugins = (array: string[]) => + array + .map(pn => Plugins[pn]) + .filter(p => p && !p.hidden) + .sort((a, b) => a.name.localeCompare(b.name)); + + const sortedPlugins = React.useMemo( + () => mapPlugins(newPlugins), + [newPlugins], + ); + + if (sortedPlugins.length === 0) { + return null; + } + + const makeDependencyList = (deps: string[]) => { + if (!deps) return null; + return ( + + This plugin is required by: + {deps.map((dep: string) => ( + + {dep} + + ))} + + ); + }; + + return ( +
+ + New Plugins ({sortedPlugins.length}) + + + + The following plugins have been added in recent updates: + + +
+ {sortedPlugins.map(plugin => { + const isRequired = + plugin.required || + depMap[plugin.name]?.some( + d => settings.plugins[d].enabled, + ) || + plugin.name.endsWith("API"); + const tooltipText = plugin.required + ? "This plugin is required for Equicord to function." + : makeDependencyList( + depMap[plugin.name]?.filter( + d => settings.plugins[d].enabled, + ), + ); + + if (isRequired) { + return ( + + {({ onMouseLeave, onMouseEnter }) => ( + + { + changes.handleChange(name); + forceUpdate(); + }} + disabled={true} + plugin={plugin} + isNew={true} + /> + + )} + + ); + } + + return ( + + { + changes.handleChange(name); + forceUpdate(); + }} + disabled={false} + plugin={plugin} + isNew={true} + /> + + ); + })} +
+ + {changes.hasChanges && ( +
+ + The following plugins require a restart: +
+
    + {changes.map(p => ( +
  • {p}
  • + ))} +
+ + } + > + {tooltipProps => ( + + )} + +
+ )} +
+ ); +} + +interface NewPluginsCompactProps { + newPlugins: string[]; + maxDisplay?: number; +} + +function CompactPluginCard({ + pluginName, + depMap, + settings, +}: { + pluginName: string; + depMap: Record; + settings: any; +}) { + const plugin = Plugins[pluginName]; + if (!plugin || plugin.hidden) return null; + + const isRequired = + plugin.required || + depMap[plugin.name]?.some(d => settings.plugins[d].enabled); + + const tooltipText = plugin.required + ? "This plugin is required for Equicord to function." + : depMap[plugin.name]?.length > 0 + ? `This plugin is required by: ${depMap[plugin.name] + ?.filter(d => settings.plugins[d].enabled) + .join(", ")}` + : null; + + return ( +
+
+ + {plugin.name} + {isRequired && " *"} + + + {plugin.authors?.[0]?.name || "Unknown"} + +
+
+ {plugin.description || "No description available"} +
+ {tooltipText && ( +
{tooltipText}
+ )} +
+ ); +} + +export function NewPluginsCompact({ + newPlugins, + maxDisplay = 20, +}: NewPluginsCompactProps) { + const settings = useSettings(); + + const depMap = React.useMemo(() => { + const o = {} as Record; + for (const plugin in Plugins) { + const deps = Plugins[plugin].dependencies; + if (deps) { + for (const dep of deps) { + o[dep] ??= []; + o[dep].push(plugin); + } + } + } + return o; + }, []); + + if (newPlugins.length === 0) { + return null; + } + + const displayPlugins = newPlugins.slice(0, maxDisplay); + const hasMore = newPlugins.length > maxDisplay; + + return ( +
+
+ {displayPlugins.map(pluginName => ( + + ))} + + {hasMore && ( +
+
+ +{newPlugins.length - maxDisplay} more plugins +
+
+ )} +
+
+ ); +} diff --git a/src/components/settings/tabs/changelog/changelogManager.ts b/src/components/settings/tabs/changelog/changelogManager.ts new file mode 100644 index 000000000..f61eb710b --- /dev/null +++ b/src/components/settings/tabs/changelog/changelogManager.ts @@ -0,0 +1,282 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; + +import gitHash from "~git-hash"; +import plugins from "~plugins"; + +export interface ChangelogEntry { + hash: string; + author: string; + message: string; + timestamp?: number; +} + +export interface UpdateSession { + id: string; + timestamp: number; + fromHash: string; + toHash: string; + commits: ChangelogEntry[]; + newPlugins: string[]; + updatedPlugins: string[]; + newSettings?: Map; + type: "update" | "repository_fetch"; +} + +export type ChangelogHistory = UpdateSession[]; + +const CHANGELOG_HISTORY_KEY = "EquicordChangelog_History"; +const LAST_SEEN_HASH_KEY = "EquicordChangelog_LastSeenHash"; +const KNOWN_PLUGINS_KEY = "EquicordChangelog_KnownPlugins"; +const KNOWN_SETTINGS_KEY = "EquicordChangelog_KnownSettings"; +const LAST_REPO_CHECK_KEY = "EquicordChangelog_LastRepoCheck"; + +type KnownPluginSettingsMap = Map>; + +export async function getChangelogHistory(): Promise { + const history = (await DataStore.get( + CHANGELOG_HISTORY_KEY, + )) as ChangelogHistory; + return history || []; +} + +export async function saveUpdateSession( + commits: ChangelogEntry[], + newPlugins: string[], + updatedPlugins: string[], + newSettings: Map, + forceLog: boolean = false, +): Promise { + const history = await getChangelogHistory(); + const lastSeenHash = await getLastSeenHash(); + const currentHash = gitHash; + + // For repository fetches, check if we already have this exact state logged (to prevent duplicate logs) + if (forceLog) { + const lastRepoCheck = await getLastRepositoryCheckHash(); + const latestRepoHash = + commits.length > 0 ? commits[0].hash : currentHash; + + if (lastRepoCheck === latestRepoHash) { + // if the state hasn't changed last check, do NOT make a new log + return; + } + } + + // Don't save if no changes, unless explicitly forcing the log (for example repository fetch) + if ( + !forceLog && + commits.length === 0 && + newPlugins.length === 0 && + updatedPlugins.length === 0 && + newSettings.size === 0 + ) { + return; + } + + // Determine session type and hash logic + const sessionType = forceLog ? "repository_fetch" : "update"; + let fromHash = currentHash; + let toHash = currentHash; + + if (forceLog) { + // This is a repository fetch - show current vs repository state + if (commits.length > 0) { + // Repository has newer commits + toHash = commits[0].hash; // Latest repository hash + } + // If no commits, fromHash === toHash (up to date) + } else { + // This is an actual update session + // Is there a better way to do this? + fromHash = lastSeenHash || "unknown"; + toHash = currentHash; + } + + const session: UpdateSession = { + id: crypto.randomUUID(), + timestamp: Date.now(), + fromHash: fromHash, + toHash: toHash, + commits, + newPlugins, + updatedPlugins, + newSettings, + type: sessionType, + }; + + // Add to beginning of history (most recent first) + history.unshift(session); + + // Keep only last 50 sessions to prevent storage bloat + if (history.length > 50) { + history.splice(50); + } + + await DataStore.set(CHANGELOG_HISTORY_KEY, history); + + if (!forceLog) { + await setLastSeenHash(currentHash); + } else { + // for fetches, check the latest repo hash to make sure its not the same + const latestRepoHash = + commits.length > 0 ? commits[0].hash : currentHash; + await setLastRepositoryCheckHash(latestRepoHash); + } + + await updateKnownPlugins(); + await updateKnownSettings(); +} + +export async function getLastSeenHash(): Promise { + return (await DataStore.get(LAST_SEEN_HASH_KEY)) as string | null; +} + +export async function setLastSeenHash(hash: string): Promise { + await DataStore.set(LAST_SEEN_HASH_KEY, hash); +} + +export async function getKnownPlugins(): Promise> { + const known = (await DataStore.get(KNOWN_PLUGINS_KEY)) as string[]; + return new Set(known || []); +} + +export async function updateKnownPlugins(): Promise { + const currentPlugins = Object.keys(plugins); + await DataStore.set(KNOWN_PLUGINS_KEY, currentPlugins); +} + +function getSettingsSetForPlugin(plugin: string): Set { + const settings = plugins[plugin]?.settings?.def || {}; + return new Set( + Object.keys(settings).filter(setting => setting !== "enabled"), + ); +} + +function getCurrentSettings(pluginList: string[]): KnownPluginSettingsMap { + return new Map( + pluginList.map(name => [name, getSettingsSetForPlugin(name)]), + ); +} + +export async function getKnownSettings(): Promise { + let map = (await DataStore.get( + KNOWN_SETTINGS_KEY, + )) as KnownPluginSettingsMap; + if (map === undefined) { + const knownPlugins = await getKnownPlugins(); + const Plugins = [...Object.keys(plugins), ...Array.from(knownPlugins)]; + map = getCurrentSettings(Plugins); + await DataStore.set(KNOWN_SETTINGS_KEY, map); + } + return map; +} + +export async function getNewSettings(): Promise> { + const map = getCurrentSettings(Object.keys(plugins)); + const knownSettings = await getKnownSettings(); + const newSettings = new Map(); + + map.forEach((settings, plugin) => { + const filteredSettings = [...settings].filter( + setting => !knownSettings.get(plugin)?.has(setting), + ); + if (filteredSettings.length > 0) { + newSettings.set(plugin, filteredSettings); + } + }); + + return newSettings; +} + +export async function updateKnownSettings(): Promise { + const currentSettings = getCurrentSettings(Object.keys(plugins)); + const knownSettings = await getKnownSettings(); + const allSettings = new Map(); + + new Set([...currentSettings.keys(), ...knownSettings.keys()]).forEach( + plugin => { + allSettings.set( + plugin, + new Set([ + ...(currentSettings.get(plugin) || []), + ...(knownSettings.get(plugin) || []), + ]), + ); + }, + ); + + await DataStore.set(KNOWN_SETTINGS_KEY, allSettings); +} + +export async function getNewPlugins(): Promise { + const currentPlugins = Object.keys(plugins); + const knownPlugins = await getKnownPlugins(); + + return currentPlugins.filter( + plugin => + !knownPlugins.has(plugin) && + !plugins[plugin].hidden && + !plugins[plugin].required, + ); +} + +export async function getUpdatedPlugins(): Promise { + // This is a placeholder - in a real implementation, you'd track plugin version changes + // For now, we'll return empty array since plugin version tracking would need to be implemented + return []; +} + +export async function clearChangelogHistory(): Promise { + await DataStore.del(CHANGELOG_HISTORY_KEY); + await DataStore.del(LAST_SEEN_HASH_KEY); + await DataStore.del(KNOWN_SETTINGS_KEY); +} + +export async function clearIndividualLog(logId: string): Promise { + const history = await getChangelogHistory(); + const filteredHistory = history.filter(log => log.id !== logId); + await DataStore.set(CHANGELOG_HISTORY_KEY, filteredHistory); +} + +export async function initializeChangelog(): Promise { + // Initialize with current state if first time + const lastSeenHash = await getLastSeenHash(); + if (!lastSeenHash) { + await setLastSeenHash(gitHash); + await updateKnownPlugins(); + await updateKnownSettings(); + } +} + +export async function getLastRepositoryCheckHash(): Promise { + return (await DataStore.get(LAST_REPO_CHECK_KEY)) as string | null; +} + +export async function setLastRepositoryCheckHash(hash: string): Promise { + await DataStore.set(LAST_REPO_CHECK_KEY, hash); +} + +export function formatTimestamp(timestamp: number): string { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMinutes < 60) { + return `${diffMinutes} minute${diffMinutes !== 1 ? "s" : ""} ago`; + } else if (diffHours < 24) { + return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`; + } else if (diffDays < 7) { + return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + } else { + return date.toLocaleDateString(); + } +} diff --git a/src/components/settings/tabs/changelog/index.tsx b/src/components/settings/tabs/changelog/index.tsx new file mode 100644 index 000000000..ee4519649 --- /dev/null +++ b/src/components/settings/tabs/changelog/index.tsx @@ -0,0 +1,693 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { ErrorCard } from "@components/ErrorCard"; +import { DeleteIcon } from "@components/Icons"; +import { Link } from "@components/Link"; +import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab"; +import { Margins } from "@utils/margins"; +import { useAwaiter } from "@utils/react"; +import { getRepo, shortGitHash, UpdateLogger } from "@utils/updater"; +import { Alerts, Button, Card, Forms, React, Toasts } from "@webpack/common"; + +import gitHash from "~git-hash"; + +import { + ChangelogEntry, + ChangelogHistory, + clearChangelogHistory, + clearIndividualLog, + formatTimestamp, + getChangelogHistory, + getLastRepositoryCheckHash, + getNewPlugins, + getNewSettings, + getUpdatedPlugins, + initializeChangelog, + saveUpdateSession, + UpdateSession, +} from "./changelogManager"; +import { NewPluginsCompact, NewPluginsSection } from "./NewPluginsSection"; + +function HashLink({ + repo, + hash, + disabled = false, +}: { + repo: string; + hash: string; + disabled?: boolean; +}) { + return ( + + {hash} + + ); +} + +function ChangelogCard({ + entry, + repo, + repoPending, +}: { + entry: ChangelogEntry; + repo: string; + repoPending: boolean; +}) { + return ( + +
+
+ + + + + by {entry.author} + +
+
+ {entry.message} +
+
+
+ ); +} + +function UpdateLogCard({ + log, + repo, + repoPending, + isExpanded, + onToggleExpand, + onClearLog, +}: { + log: UpdateSession; + repo: string; + repoPending: boolean; + isExpanded: boolean; + onToggleExpand: () => void; + onClearLog: (logId: string) => void; +}) { + const isRepositoryFetch = + log.type === "repository_fetch" || + (log.type === undefined && + log.fromHash === log.toHash && + log.commits.length === 0); + const isUpToDate = log.fromHash === log.toHash; + + return ( + +
+
+
+ + {isRepositoryFetch + ? isUpToDate + ? `Repository check: ${log.fromHash.slice(0, 7)} (up to date)` + : `Repository check: ${log.fromHash.slice(0, 7)} → ${log.toHash.slice(0, 7)}` + : `Update: ${log.fromHash.slice(0, 7)} → ${log.toHash.slice(0, 7)}`} + + +
+
+ {formatTimestamp(log.timestamp)} + {log.commits.length > 0 && + ` • ${log.commits.length} commits available`} + {log.commits.length === 0 && " • No new commits"} + {log.newPlugins.length > 0 && + ` • ${log.newPlugins.length} new plugins`} + {log.updatedPlugins.length > 0 && + ` • ${log.updatedPlugins.length} updated plugins`} + {log.newSettings && + log.newSettings.size > 0 && + ` • ${Array.from(log.newSettings.values()).reduce((sum, arr) => sum + arr.length, 0)} new settings`} +
+
+
+ ▼ +
+
+ + {isExpanded && ( +
+ {log.newPlugins.length > 0 && ( +
+ +
+ )} + + {log.updatedPlugins.length > 0 && ( +
+ + Updated Plugins + + +
+ )} + + {log.newSettings && log.newSettings.size > 0 && ( +
+ + New Settings + +
+ {Array.from( + log.newSettings?.entries() || [], + ).map(([pluginName, settings]) => + settings.map(setting => ( + + {pluginName}.{setting} + + )), + )} +
+
+ )} + + {log.commits.length > 0 && ( +
+
+ {log.commits.map(entry => ( + + ))} +
+
+ )} +
+ )} +
+ ); +} + +function ChangelogContent() { + const [repo, repoErr, repoPending] = useAwaiter(getRepo, { + fallbackValue: "Loading...", + }); + const [changelog, setChangelog] = React.useState([]); + const [changelogHistory, setChangelogHistory] = + React.useState([]); + const [newPlugins, setNewPlugins] = React.useState([]); + const [updatedPlugins, setUpdatedPlugins] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(null); + const [expandedLogs, setExpandedLogs] = React.useState>( + new Set(), + ); + const [showHistory, setShowHistory] = React.useState(false); + const [recentlyChecked, setRecentlyChecked] = React.useState(false); + + React.useEffect(() => { + const init = async () => { + try { + await initializeChangelog(); + await loadChangelogHistory(); + } catch (err) { + console.error("Failed to initialize changelog:", err); + } + }; + init(); + }, []); + + React.useEffect(() => { + if (repoErr) { + UpdateLogger.error("Failed to retrieve repo", repoErr); + setError("Failed to retrieve repository information"); + } + }, [repoErr]); + + const loadChangelogHistory = React.useCallback(async () => { + try { + const history = await getChangelogHistory(); + setChangelogHistory(history); + } catch (err) { + console.error("Failed to load changelog history:", err); + } + }, []); + + const loadNewPlugins = React.useCallback(async () => { + try { + const newPlgs = await getNewPlugins(); + const updatedPlgs = await getUpdatedPlugins(); + setNewPlugins(newPlgs); + setUpdatedPlugins(updatedPlgs); + } catch (err) { + console.error("Failed to load new plugins:", err); + } + }, []); + + // check if the repository was recently refreshed + React.useEffect(() => { + const checkRecentStatus = async () => { + try { + const lastRepoCheck = await getLastRepositoryCheckHash(); + const updates = await VencordNative.updater.getUpdates(); + + if (updates.ok) { + const currentRepoHash = + updates.value.length > 0 + ? updates.value[0].hash + : gitHash; + setRecentlyChecked(lastRepoCheck === currentRepoHash); + } + } catch (err) { + // ignore errors (hopefully there are none lol) + setRecentlyChecked(false); + } + }; + + if (!repoPending && !repoErr) { + checkRecentStatus(); + } + }, [repoPending, repoErr]); + + const fetchChangelog = React.useCallback(async () => { + if (repoPending || repoErr) return; + + setIsLoading(true); + setError(null); + + try { + // check if the repository was recently refreshed and that nothing has changed + const updates = await VencordNative.updater.getUpdates(); + const lastRepoCheck = await getLastRepositoryCheckHash(); + const currentRepoHash = + updates.ok && updates.value.length > 0 + ? updates.value[0].hash + : gitHash; + + // If repository state hasn't changed since last check + if (lastRepoCheck === currentRepoHash) { + setIsLoading(false); + setRecentlyChecked(true); + Toasts.show({ + message: "Already up to date with repository", + id: Toasts.genId(), + type: Toasts.Type.MESSAGE, + options: { + position: Toasts.Position.BOTTOM, + }, + }); + return; + } + + + if (updates.ok && updates.value) { + setChangelog(updates.value); + + // Load current new/updated plugins and settings + const newPlgs = await getNewPlugins(); + const updatedPlgs = await getUpdatedPlugins(); + const newSettings = await getNewSettings(); + setNewPlugins(newPlgs); + setUpdatedPlugins(updatedPlgs); + + // always save the current fetch session to history to track repo state + await saveUpdateSession( + updates.value, + newPlgs, + updatedPlgs, + newSettings, + true, // forceLog = true for repository fetches + ); + await loadChangelogHistory(); + setRecentlyChecked(true); + + Toasts.show({ + message: + updates.value.length > 0 + ? `Found ${updates.value.length} commit${updates.value.length === 1 ? "" : "s"} from repository` + : "Repository is up to date with your local copy", + id: Toasts.genId(), + type: + updates.value.length > 0 + ? Toasts.Type.SUCCESS + : Toasts.Type.MESSAGE, + options: { + position: Toasts.Position.BOTTOM, + }, + }); + } else if (!updates.ok) { + throw new Error( + updates.error?.message || "Failed to fetch from repository", + ); + } + } catch (err: any) { + UpdateLogger.error("Failed to fetch commits from repository", err); + const errorMessage = + err?.message || + "Failed to connect to repository. Check your internet connection."; + setError(errorMessage); + + // funny little error toast hopefully doesn't happen! + Toasts.show({ + message: "Could not fetch commits from repository", + id: Toasts.genId(), + type: Toasts.Type.FAILURE, + options: { + position: Toasts.Position.BOTTOM, + }, + }); + } finally { + setIsLoading(false); + } + }, [repoPending, repoErr, loadNewPlugins, loadChangelogHistory]); + + React.useEffect(() => { + const loadInitialData = async () => { + if (!repoPending && !repoErr) { + // load new plugins first, then fetch the commits + await loadNewPlugins(); + await fetchChangelog(); + } else if (!repoPending) { + // perseverance + await loadNewPlugins(); + setIsLoading(false); + } + }; + + loadInitialData(); + }, [repoPending, repoErr, fetchChangelog, loadNewPlugins]); + + const toggleLogExpanded = (logId: string) => { + const newExpanded = new Set(expandedLogs); + if (newExpanded.has(logId)) { + newExpanded.delete(logId); + } else { + newExpanded.add(logId); + } + setExpandedLogs(newExpanded); + }; + + const hasCurrentChanges = + changelog.length > 0 || + newPlugins.length > 0 || + updatedPlugins.length > 0; + + return ( + <> + + View the most recent changes to Equicord. This fetches commits + directly from the repository to show you what's new. + + + Repository + + {repoPending ? ( + repo + ) : repoErr ? ( + "Failed to retrieve - check console" + ) : ( + + {repo.split("/").slice(-2).join("/")} + + )}{" "} + (Current:{" "} + + {shortGitHash()} + + ) + + +
+ + + {changelogHistory.length > 0 && ( + <> + + + + )} +
+ + {error && ( + +

{error}

+ + Make sure you have an internet connection and try again. + +
+ )} + + + + {/* Current Changes Section */} + {hasCurrentChanges ? ( +
+ + Recent Changes + + + {/* New Plugins Section */} + {newPlugins.length > 0 && ( +
+ { + // Handle plugin toggle if needed + }} + /> +
+ )} + + {/* Updated Plugins Section */} + {updatedPlugins.length > 0 && ( +
+ + Updated Plugins ({updatedPlugins.length}) + + +
+ )} + + {/* Code Changes */} + {changelog.length > 0 && ( +
+ + Code Changes ({changelog.length}{" "} + {changelog.length === 1 ? "commit" : "commits"}) + +
+ {changelog.map(entry => ( + + ))} +
+
+ )} +
+ ) : ( + !isLoading && + !error && ( + + + No commits available ahead of your current version. + Click "Fetch from Repository" to check for new + changes. + + + ) + )} + + {/* History Section */} + {showHistory && changelogHistory.length > 0 && ( +
+ + + Update Logs ({changelogHistory.length}{" "} + {changelogHistory.length === 1 ? "log" : "logs"}) + + + Previous update sessions with their commit history and + plugin changes. + + +
+ {changelogHistory.map(log => ( + toggleLogExpanded(log.id)} + onClearLog={logId => { + Alerts.show({ + title: "Clear Log", + body: "Are you sure you would like to clear this log? This can't be undone.", + confirmText: "Clear Log", + confirmColor: "danger", + cancelText: "Cancel", + onConfirm: async () => { + await clearIndividualLog(logId); + await loadChangelogHistory(); + setExpandedLogs( + new Set( + Array.from( + expandedLogs, + ).filter( + id => id !== logId, + ), + ), + ); + Toasts.show({ + message: "Log has been cleared", + id: Toasts.genId(), + type: Toasts.Type.SUCCESS, + options: { + position: + Toasts.Position.BOTTOM, + }, + }); + }, + }); + }} + /> + ))} +
+
+ )} + + ); +} + +function ChangelogTab() { + return ( + + + + ); +} + +export default wrapTab(ChangelogTab, "Changelog"); diff --git a/src/components/settings/tabs/changelog/styles.css b/src/components/settings/tabs/changelog/styles.css new file mode 100644 index 000000000..4935027de --- /dev/null +++ b/src/components/settings/tabs/changelog/styles.css @@ -0,0 +1,572 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/* Existing changelog styles */ +.vc-changelog-entry { + padding: 0.75em; + margin-bottom: 0.5em; + background-color: var(--background-base-lower-alt); + border-radius: 8px; + transition: 0.1s ease-out; + transition-property: box-shadow, transform, background, opacity; +} + +.vc-changelog-entry:hover { + background-color: var(--background-tertiary); + transform: translateY(-1px); + box-shadow: var(--elevation-high); +} + +.vc-changelog-entry.required { + border-left: 4px solid var(--status-warning); + background-color: var(--background-base-lower-alt); + padding-left: calc(0.75em - 4px); +} + +.vc-changelog-entry.required:hover { + background-color: var(--background-tertiary); + border-left-color: var(--status-warning-foreground); +} + +.vc-changelog-entry-header { + display: flex; + align-items: center; + gap: 0.5em; + margin-bottom: 0.25em; +} + +.vc-changelog-entry-hash { + font-size: 0.875em; + font-family: var(--font-code); + color: var(--text-muted); +} + +.vc-changelog-entry-author { + font-size: 0.875em; + color: var(--text-muted); + font-weight: 500; +} + +.vc-changelog-entry-message { + color: var(--text-default); + font-size: 0.95em; + line-height: 1.4; +} + +.vc-changelog-entry.required .vc-changelog-entry-hash { + color: var(--text-muted); + font-weight: 600; +} + +.vc-changelog-loading { + text-align: center; + padding: 2em; + color: var(--text-muted); +} + +.vc-changelog-empty { + text-align: center; + padding: 2em; + background-color: var(--background-base-lower-alt); + border-radius: 8px; + color: var(--text-muted); +} + +.vc-changelog-controls { + margin-bottom: 1em; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; +} + +.vc-changelog-repo-info { + display: flex; + align-items: center; + gap: 0.5em; + margin-bottom: 1em; +} + +.vc-changelog-current-hash { + font-family: var(--font-code); + background-color: var(--background-base-lowest); + padding: 0.125em 0.25em; + border-radius: 3px; + font-size: 0.875em; +} + +/* New Plugins Section Styles */ +.vc-changelog-new-plugins-section { + margin-bottom: 1.5em; + padding: 1em; + background-color: var(--background-base-lower-alt); + border-radius: 8px; + border-left: 4px solid var(--brand); +} + +.vc-changelog-new-plugins-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 0.75em; +} + +.vc-changelog-new-plugin-card { + background-color: var(--background-tertiary); + border-radius: 6px; + padding: 0.5em; + transition: all 0.1s ease-out; +} + +.vc-changelog-new-plugin-card:hover { + background-color: var(--background-base-lowest); + transform: translateY(-1px); + box-shadow: var(--elevation-medium); +} + +.vc-changelog-new-plugin-card.required { + border-left: 4px solid var(--status-warning); + background-color: var(--background-base-lower-alt); +} + +.vc-changelog-new-plugin-card.required:hover { + background-color: var(--background-tertiary); + border-left-color: var(--status-warning-foreground); +} + +.vc-changelog-new-plugins-compact { + display: flex; + flex-wrap: wrap; + gap: 0.5em; + margin-bottom: 0; + padding-top: 0; +} + +.vc-changelog-new-plugins-list { + display: flex; + flex-wrap: wrap; + gap: 0.5em; +} + +.vc-changelog-new-plugin-tag { + display: inline-flex; + align-items: center; + padding: 0.25em 0.5em; + background-color: var(--brand); + color: var(--white); + border-radius: 4px; + font-size: 0.875em; + font-weight: 500; + text-decoration: none; + transition: background-color 0.1s ease; +} + +.vc-changelog-new-plugin-tag:hover { + background-color: var(--brand-700); +} + +.vc-changelog-new-plugin-tag.more { + background-color: var(--background-base-lower-alt); + color: var(--text-muted); +} + +.vc-changelog-new-plugin-tag.required { + background-color: var(--status-warning); + color: var(--white); + border: 1px solid var(--status-warning-foreground); +} + +.vc-changelog-new-plugin-tag.required:hover { + background-color: var(--status-warning-foreground); +} + +.vc-changelog-restart-notice { + margin-top: 1em; + padding: 0.75em; + background-color: var(--info-warning-background); + border-radius: 6px; + border-left: 4px solid var(--status-warning); +} + +/* Update Logs Styles */ +.vc-changelog-history { + margin-top: 1.5em; +} + +.vc-changelog-history-list { + display: flex; + flex-direction: column; + gap: 0.75em; + padding-right: 0.5em; + min-height: auto !important; + max-height: none !important; + height: auto !important; +} + +.vc-changelog-log { + background-color: var(--background-base-lower-alt); + border-radius: 8px; + overflow: hidden; + transition: all 0.1s ease-out; +} + +.vc-changelog-log:hover { + box-shadow: var(--elevation-medium); +} + +.vc-changelog-log-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1em; + cursor: pointer; + transition: background-color 0.1s ease; + user-select: none; + gap: 0.5em; + position: relative; +} + +.vc-changelog-log-header:hover { + background-color: var(--background-tertiary); +} + +.vc-changelog-log-info { + flex: 1; +} + +.vc-changelog-log-title { + font-weight: 600; + font-size: 0.95em; + color: var(--text-default); + margin-bottom: 0.25em; + font-family: var(--font-code); + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.vc-changelog-log-meta { + font-size: 0.875em; + color: var(--text-muted); + line-height: 1.3; +} + +.vc-changelog-log-toggle { + transition: transform 0.2s ease; + color: var(--text-muted); + font-size: 0.75em; + flex-shrink: 0; +} + +.vc-changelog-log-toggle.expanded { + transform: rotate(180deg); +} + +.vc-changelog-log-content { + padding: 0 1em 1em; + border-top: 1px solid var(--background-base-lowest); + animation: expand-content 0.2s ease-out; + + /* allow content to expand naturally (fixes cramming of elements) */ + min-height: auto !important; + max-height: none !important; + overflow: visible !important; +} + +.vc-changelog-log-plugins { + margin-bottom: 1em; + padding-top: 0.75em; +} + +.vc-changelog-log-plugins:last-child { + margin-bottom: 0; +} + +.vc-changelog-log-commits { + margin-bottom: 1em; + padding-top: 0.75em; +} + +.vc-changelog-log-commits:last-child { + margin-bottom: 0; +} + +.vc-changelog-log-commits-list { + display: flex; + flex-direction: column; + gap: 0.5em; + padding-right: 0.5em; + padding-top: 0; +} + +/* fix plugin entry padding to match commit entries (this took 3 different tries bro) */ +.vc-changelog-plugins-list .vc-changelog-entry { + padding: 0.75em; + margin-bottom: 0.5em; +} + +/* Visual refresh support */ +.visual-refresh .vc-changelog-entry { + background-color: var(--card-primary-bg); + border: 1px solid var(--border-subtle); +} + +.vc-changelog-log-commits-list .vc-changelog-entry:last-child { + margin-bottom: 0; +} + +.vc-changelog-plugins-list { + display: flex; + flex-direction: column; + gap: 0.5em; + padding-right: 0.5em; + padding-top: 0; +} + +.visual-refresh .vc-changelog-entry.required { + border-left: 4px solid var(--status-warning); + background-color: var(--card-primary-bg); + border: 1px solid var(--border-subtle); + padding-left: calc(0.75em - 4px); +} + +.vc-changelog-plugins-list .vc-changelog-entry:last-child { + margin-bottom: 0; +} + +/* consistent padding for required plugin entries in logs */ +.vc-changelog-plugins-list .vc-changelog-entry.required { + padding-left: calc(0.75em - 4px); + margin-bottom: 0.5em; +} + +.vc-changelog-plugins-list .vc-changelog-entry.required:last-child { + margin-bottom: 0; +} + +.vc-changelog-log-commits-list::-webkit-scrollbar { + width: 6px; +} + +.vc-changelog-log-commits-list::-webkit-scrollbar-track { + background: var(--background-base-lower-alt); + border-radius: 3px; +} + +.vc-changelog-log-commits-list::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 3px; +} + +.vc-changelog-log-commits-list::-webkit-scrollbar-thumb:hover { + background: var(--text-default); +} + +/* Current changes section */ +.vc-changelog-current { + margin-bottom: 1.5em; +} + +.vc-changelog-commits-list { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +/* Scrollbar styling for history list and log content */ +.vc-changelog-history-list::-webkit-scrollbar, +.vc-changelog-log-content::-webkit-scrollbar { + width: 6px; +} + +.vc-changelog-history-list::-webkit-scrollbar-track, +.vc-changelog-log-content::-webkit-scrollbar-track { + background: var(--background-base-lower-alt); + border-radius: 3px; +} + +.vc-changelog-history-list::-webkit-scrollbar-thumb, +.vc-changelog-log-content::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 3px; +} + +.vc-changelog-history-list::-webkit-scrollbar-thumb:hover, +.vc-changelog-log-content::-webkit-scrollbar-thumb:hover { + background: var(--text-default); +} + +.visual-refresh .vc-changelog-entry:hover { + background-color: var(--card-primary-bg); + box-shadow: 0 2px 8px rgb(0 0 0 / 10%); +} + +.visual-refresh .vc-changelog-empty { + background-color: var(--card-primary-bg); + border: 1px solid var(--border-subtle); +} + +.visual-refresh .vc-changelog-current-hash { + background-color: var(--input-background); + border: 1px solid var(--border-subtle); +} + +.visual-refresh .vc-changelog-new-plugins-section { + background-color: var(--card-primary-bg); + border: 1px solid var(--border-subtle); + border-left: 4px solid var(--brand); +} + +.visual-refresh .vc-changelog-new-plugin-card { + background-color: var(--card-secondary-bg); + border: 1px solid var(--border-subtle); +} + +.visual-refresh .vc-changelog-log { + background-color: var(--card-primary-bg); + border: 1px solid var(--border-subtle); +} + +.visual-refresh .vc-changelog-log-header:hover { + background-color: var(--card-secondary-bg); +} + +.visual-refresh .vc-changelog-log-content { + border-top: 1px solid var(--border-subtle); +} + +.visual-refresh .vc-changelog-restart-notice { + background-color: var(--card-primary-bg); + border: 1px solid var(--status-warning); + border-left: 4px solid var(--status-warning); +} + +/* Responsive design */ +@media (width <=768px) { + .vc-changelog-new-plugins-grid { + grid-template-columns: 1fr; + } + + .vc-changelog-controls { + flex-direction: column; + align-items: stretch; + } + + .vc-changelog-session-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5em; + } + + .vc-changelog-log-toggle { + align-self: flex-end; + margin-top: -1.5em; + } + + .vc-changelog-log-title { + flex-wrap: wrap; + gap: 4px; + } + + .vc-changelog-delete-button { + order: 1; + } +} + +/* Animation for expanding/collapsing logs */ + +@keyframes expand-content { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Badge styles for plugin counts */ +.vc-changelog-badge { + display: inline-flex; + align-items: center; + padding: 0.125em 0.375em; + background-color: var(--brand); + color: var(--white); + border-radius: 12px; + font-size: 0.75em; + font-weight: 600; + margin-left: 0.5em; +} + +.vc-changelog-badge.warning { + background-color: var(--status-warning); +} + +.vc-changelog-badge.success { + background-color: var(--status-positive); +} + +/* Required plugin styling */ +.vc-changelog-dep-text { + font-size: 0.875em; + color: var(--text-muted); + margin-left: 1em; + font-style: italic; +} + +/* Visual refresh support for required plugins */ +.visual-refresh .vc-changelog-new-plugin-tag.required { + background-color: var(--status-warning); + border: 1px solid var(--status-warning-foreground); +} + +.visual-refresh .vc-changelog-new-plugin-card.required { + border-left: 4px solid var(--status-warning); + background-color: var(--card-secondary-bg); +} + +/* Global overrides for changelog in settings modal */ +.vc-settings-tab [class*="changelog"] { + min-height: auto !important; + max-height: none !important; + overflow: visible !important; +} + +/* Ensure Forms.FormSection doesn't constrain changelog */ +.vc-settings-tab .vc-changelog-history, +.vc-settings-tab .vc-changelog-history-list, +.vc-settings-tab .vc-changelog-log-content { + min-height: auto !important; + max-height: none !important; + height: auto !important; + overflow: visible !important; +} + +.visual-refresh .vc-changelog-entry.required:hover { + background-color: var(--card-secondary-bg); +} + +/* Trash icon styling */ +.vc-changelog-delete-button { + transition: + opacity 0.2s ease, + color 0.2s ease, + background-color 0.2s ease; + border-radius: 4px; + flex-shrink: 0; +} + +.vc-changelog-delete-button:hover { + opacity: 1 !important; + color: var(--status-danger) !important; + background-color: var(--background-modifier-hover); +} + +.vc-changelog-log-header:hover .vc-changelog-delete-button { + opacity: 0.8; +} \ No newline at end of file diff --git a/src/components/settings/tabs/index.ts b/src/components/settings/tabs/index.ts index bad044984..ae31d26a4 100644 --- a/src/components/settings/tabs/index.ts +++ b/src/components/settings/tabs/index.ts @@ -16,3 +16,4 @@ export { default as BackupAndRestoreTab } from "./sync/BackupAndRestoreTab"; export { default as CloudTab } from "./sync/CloudTab"; export { openUpdaterModal, default as UpdaterTab } from "./updater"; export { default as VencordTab } from "./vencord"; +export { default as ChangelogTab } from "./changelog"; diff --git a/src/equicordplugins/newPluginsManager/NewPluginsModal.css b/src/equicordplugins/newPluginsManager/NewPluginsModal.css deleted file mode 100644 index 163505ae7..000000000 --- a/src/equicordplugins/newPluginsManager/NewPluginsModal.css +++ /dev/null @@ -1,20 +0,0 @@ -.vc-newPluginsManager-restart-tooltip { - max-width: 280px; -} - -.vc-newPluginsManager-restart-list { - padding-left: 1.25em; -} - -.vc-newPluginsManager-restart-list li { - list-style: disc; -} - -.vc-newPluginsManager-close > div { - display: flex; -} - -.vc-newPluginsManager-disable-wrapper { - display: flex; - margin: auto 0; -} \ No newline at end of file diff --git a/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx b/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx deleted file mode 100644 index 9f75aefbb..000000000 --- a/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./NewPluginsModal.css"; - -import { Settings, useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { PluginCard } from "@components/settings/tabs/plugins/PluginCard"; -import { ChangeList } from "@utils/ChangeList"; -import { classes, Margins } from "@utils/index"; -import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { useForceUpdater } from "@utils/react"; -import { findComponentByCodeLazy } from "@webpack"; -import { Button, Flex, Forms, React, Text, Tooltip, useMemo } from "@webpack/common"; -import { JSX } from "react"; - -import Plugins from "~plugins"; - -import { getNewPlugins, getNewSettings, KnownPluginSettingsMap, writeKnownSettings } from "./knownSettings"; - -const cl = classNameFactory("vc-plugins-"); - -const Checkbox = findComponentByCodeLazy(".checkboxWrapperDisabled:"); - -let hasSeen = false; - -// Most of this was stolen from PluginSettings directly. - -export function NewPluginsModal({ modalProps, newPlugins, newSettings }: { modalProps: ModalProps; newPlugins: Set; newSettings: KnownPluginSettingsMap; }) { - const settings = useSettings(); - const changes = React.useMemo(() => new ChangeList(), []); - let updateContinueButton = () => { }; - - const depMap = React.useMemo(() => { - const o = {} as Record; - for (const plugin in Plugins) { - const deps = Plugins[plugin].dependencies; - if (deps) { - for (const dep of deps) { - o[dep] ??= []; - o[dep].push(plugin); - } - } - } - return o; - }, []); - - const mapPlugins = (array: string[]) => array.map(pn => Plugins[pn]) - .sort((a, b) => a.name.localeCompare(b.name)); - - const sortedPlugins = useMemo(() => [ - ...mapPlugins([...newPlugins]), - ...mapPlugins([...newSettings.keys()].filter(p => !newPlugins.has(p))) - ], []); - - const plugins = [] as JSX.Element[]; - const requiredPlugins = [] as JSX.Element[]; - - for (const p of sortedPlugins) { - if (p.hidden) - continue; - - const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled); - - if (isRequired) { - const tooltipText = p.required - ? "This plugin is required for Vencord to function." - : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); - - requiredPlugins.push( - - {({ onMouseLeave, onMouseEnter }) => ( - { - changes.handleChange(name); - updateContinueButton(); - }} - disabled={true} - plugin={p} - key={p.name} - isNew={newPlugins.has(p.name)} - /> - )} - - ); - } else { - plugins.push( - { - changes.handleChange(name); - updateContinueButton(); - }} - disabled={false} - plugin={p} - key={p.name} - isNew={newPlugins.has(p.name)} - /> - ); - } - } - - - return - - New Plugins and Settings ({[...plugins, ...requiredPlugins].length}) - - {tooltipProps => -
- -
- } -
-
- -
- {[...plugins, ...requiredPlugins]} -
-
- - - void) => updateContinueButton = v} - /> - - -
- { - Settings.plugins.NewPluginsManager.enabled = !settings?.plugins?.NewPluginsManager?.enabled; - }} - > - Don't show this again - -
-
-
-
; -} - -function ContinueButton(props: { callback: (update: () => void) => void; changes: ChangeList; close: () => any; }) { - const update = useForceUpdater(); - props.callback(update); - return - The following plugins require a restart: -
-
    - {props.changes.map(p =>
  • {p}
  • )} -
- } - shouldShow={props.changes.hasChanges} - > - {tooltipProps => - - } - ; -} - - -function makeDependencyList(deps: string[]) { - return ( - - This plugin is required by: - {deps.map((dep: string) => {dep})} - - ); -} - -export async function openNewPluginsModal() { - const newPlugins = await getNewPlugins(); - const newSettings = await getNewSettings(); - if ((newPlugins.size || newSettings.size) && !hasSeen) { - hasSeen = true; - const modalKey = openModal(modalProps => { closeModal(modalKey); }}> - - ); - } -} diff --git a/src/equicordplugins/newPluginsManager/index.tsx b/src/equicordplugins/newPluginsManager/index.tsx deleted file mode 100644 index 683d9de74..000000000 --- a/src/equicordplugins/newPluginsManager/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -import * as KnownSettings from "./knownSettings"; -import { KNOWN_PLUGINS_LEGACY_DATA_KEY, KNOWN_SETTINGS_DATA_KEY } from "./knownSettings"; -import { openNewPluginsModal } from "./NewPluginsModal"; - -export default definePlugin({ - name: "NewPluginsManager", - description: "Utility that notifies you when new plugins are added to Equicord", - authors: [Devs.Sqaaakoi], - enabledByDefault: true, - flux: { - async POST_CONNECTION_OPEN() { - openNewPluginsModal(); - } - }, - openNewPluginsModal, - KNOWN_PLUGINS_LEGACY_DATA_KEY, - KNOWN_SETTINGS_DATA_KEY, - KnownSettings -}); diff --git a/src/equicordplugins/newPluginsManager/knownSettings.ts b/src/equicordplugins/newPluginsManager/knownSettings.ts deleted file mode 100644 index 397d1390a..000000000 --- a/src/equicordplugins/newPluginsManager/knownSettings.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { DataStore } from "@api/index"; - -import plugins from "~plugins"; - -export type KnownPluginSettingsMap = Map>; - -export const KNOWN_PLUGINS_LEGACY_DATA_KEY = "NewPluginsManager_KnownPlugins"; -export const KNOWN_SETTINGS_DATA_KEY = "NewPluginsManager_KnownSettings"; - -function getSettingsSetForPlugin(plugin: string): Set { - const settings = plugins[plugin]?.settings?.def || {}; - return new Set(Object.keys(settings).filter(setting => setting !== "enabled")); -} - -function getCurrentSettings(pluginList: string[]): KnownPluginSettingsMap { - return new Map(pluginList.map(name => [ - name, - getSettingsSetForPlugin(name) - ])); -} - -export async function getKnownSettings(): Promise { - let map = await DataStore.get(KNOWN_SETTINGS_DATA_KEY) as KnownPluginSettingsMap; - if (map === undefined) { - const knownPlugins = await DataStore.get(KNOWN_PLUGINS_LEGACY_DATA_KEY) ?? [] as string[]; - const Plugins = [...Object.keys(plugins), ...knownPlugins]; - map = getCurrentSettings(Plugins); - DataStore.set(KNOWN_SETTINGS_DATA_KEY, map); - } - return map; -} - -export async function getNewSettings(): Promise { - const map = getCurrentSettings(Object.keys(plugins)); - const knownSettings = await getKnownSettings(); - map.forEach((settings, plugin) => { - const filteredSettings = [...settings].filter(setting => !knownSettings.get(plugin)?.has(setting)); - if (!filteredSettings.length) return map.delete(plugin); - map.set(plugin, new Set(filteredSettings)); - }); - return map; -} - -export async function getKnownPlugins(): Promise> { - const knownSettings = await getKnownSettings(); - return new Set(knownSettings.keys()); -} - -export async function getNewPlugins(): Promise> { - const currentPlugins = Object.keys(plugins); - const knownPlugins = await getKnownPlugins(); - return new Set(currentPlugins.filter(p => !knownPlugins.has(p))); -} - -export async function writeKnownSettings() { - const currentSettings = getCurrentSettings(Object.keys(plugins)); - const knownSettings = await getKnownSettings(); - const allSettings = new Map(); - new Set([...currentSettings.keys(), ...knownSettings.keys()]).forEach(plugin => { - allSettings.set(plugin, new Set([ - ...(currentSettings.get(plugin) || []), - ...(knownSettings.get(plugin) || []) - ])); - }); - await DataStore.set(KNOWN_SETTINGS_DATA_KEY, allSettings); -} - -export async function debugWipeSomeData() { - const settings = await getKnownSettings(); - settings.forEach((value, key) => { - if (Math.random() > 0.8) { - if (Math.random() > 0.5) return settings.set(key, new Set([...value].filter(() => Math.random() > 0.5))); - return settings.delete(key); - } - }); - await DataStore.set(KNOWN_SETTINGS_DATA_KEY, settings); -} - -export async function editRawData(patcher: (data: KnownPluginSettingsMap) => (Promise | any)) { - if (!patcher) return; - const map = await DataStore.get(KNOWN_SETTINGS_DATA_KEY) as KnownPluginSettingsMap; - const newMap = new Map(map); - await patcher(newMap); - await DataStore.set(KNOWN_SETTINGS_DATA_KEY, newMap ?? map); -} diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 974cf7d4a..fe9ba59f8 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -1,23 +1,19 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and Megumin - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ import { Settings } from "@api/Settings"; -import { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, UpdaterTab, VencordTab } from "@components/settings"; +import { + BackupAndRestoreTab, + ChangelogTab, + CloudTab, + PatchHelperTab, + PluginsTab, + UpdaterTab, + VencordTab, +} from "@components/settings"; import ThemesTab from "@components/ThemeSettings/ThemesTab"; import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; @@ -25,7 +21,6 @@ import definePlugin, { OptionType } from "@utils/types"; import { shortGitHash } from "@utils/updater"; import { React } from "@webpack/common"; - type SectionType = "HEADER" | "DIVIDER" | "CUSTOM"; type SectionTypes = Record; @@ -44,26 +39,34 @@ export default definePlugin({ replace: (m, component, props) => { props = props.replace(/children:\[.+\]/, ""); return `${m},$self.makeInfoElements(${component}, ${props})`; - } + }, }, { match: /copyValue:\i\.join\(" "\)/, - replace: "$& + $self.getInfoString()" - } - ] + replace: "$& + $self.getInfoString()", + }, + ], }, { find: ".SEARCH_NO_RESULTS&&0===", replacement: [ { match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/, - replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}` + replace: ( + _, + sectionTypes, + commaOrSemi, + elements, + element, + ) => + `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`, }, { match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?\(\)=>)\2/, - replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})` - } - ] + replace: (_, rest, settingsHook) => + `${rest}$self.wrapSettingsHook(${settingsHook})`, + }, + ], }, { find: ".DEVELOPER_SECTION,", @@ -78,9 +81,9 @@ export default definePlugin({ find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}", replacement: { match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.\i\).*?(\i\.\i)\.open\()/, - replace: "$2.open($1);return;" - } - } + replace: "$2.open($1);return;", + }, + }, ], customSections: [] as ((SectionTypes: SectionTypes) => any)[], @@ -90,72 +93,87 @@ export default definePlugin({ { section: SectionTypes.HEADER, label: "Equicord", - className: "vc-settings-header" + className: "vc-settings-header", }, { section: "EquicordSettings", label: "Equicord", element: VencordTab, - className: "vc-settings" + className: "vc-settings", }, { section: "EquicordPlugins", label: "Plugins", searchableTitles: ["Plugins"], element: PluginsTab, - className: "vc-plugins" + className: "vc-plugins", }, { section: "EquicordThemes", label: "Themes", searchableTitles: ["Themes"], element: ThemesTab, - className: "vc-themes" + className: "vc-themes", }, !IS_UPDATER_DISABLED && { section: "EquicordUpdater", label: "Updater", searchableTitles: ["Updater"], element: UpdaterTab, - className: "vc-updater" + 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" + className: "vc-cloud", }, { section: "settings/tabsSync", label: "Backup & Restore", searchableTitles: ["Backup & Restore"], element: BackupAndRestoreTab, - className: "vc-backup-restore" + className: "vc-backup-restore", }, IS_DEV && { section: "EquicordPatchHelper", label: "Patch Helper", searchableTitles: ["Patch Helper"], element: PatchHelperTab, - className: "vc-patch-helper" + className: "vc-patch-helper", }, ...this.customSections.map(func => func(SectionTypes)), { - section: SectionTypes.DIVIDER - } + section: SectionTypes.DIVIDER, + }, ].filter(Boolean); }, - isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) { + isRightSpot({ + header, + settings, + }: { + header?: string; + settings?: string[]; + }) { const firstChild = settings?.[0]; // lowest two elements... sanity backup - if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true; + if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") + return true; const { settingsLocation } = Settings.plugins.Settings; if (settingsLocation === "bottom") return firstChild === "LOGOUT"; - if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG"; + if (settingsLocation === "belowActivity") + return firstChild === "CHANGELOG"; if (!header) return; @@ -164,10 +182,13 @@ export default definePlugin({ top: getIntlMessage("USER_SETTINGS"), aboveNitro: getIntlMessage("BILLING_SETTINGS"), belowNitro: getIntlMessage("APP_SETTINGS"), - aboveActivity: getIntlMessage("ACTIVITY_SETTINGS") + aboveActivity: getIntlMessage("ACTIVITY_SETTINGS"), }; - if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS")) + if ( + !names[settingsLocation] || + names[settingsLocation].endsWith("_SETTINGS") + ) return firstChild === "PREMIUM"; return header === names[settingsLocation]; @@ -178,23 +199,32 @@ export default definePlugin({ patchedSettings: new WeakSet(), - addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: SectionTypes) { - if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return; + addSettings( + elements: any[], + element: { header?: string; settings: string[]; }, + sectionTypes: SectionTypes, + ) { + if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) + return; this.patchedSettings.add(elements); elements.push(...this.makeSettingsCategories(sectionTypes)); }, - wrapSettingsHook(originalHook: (...args: any[]) => Record[]) { + wrapSettingsHook( + originalHook: (...args: any[]) => Record[], + ) { return (...args: any[]) => { const elements = originalHook(...args); if (!this.patchedSettings.has(elements)) - elements.unshift(...this.makeSettingsCategories({ - HEADER: "HEADER", - DIVIDER: "DIVIDER", - CUSTOM: "CUSTOM" - })); + elements.unshift( + ...this.makeSettingsCategories({ + HEADER: "HEADER", + DIVIDER: "DIVIDER", + CUSTOM: "CUSTOM", + }), + ); return elements; }; @@ -211,20 +241,29 @@ export default definePlugin({ { label: "Above Activity Settings", value: "aboveActivity" }, { label: "Below Activity Settings", value: "belowActivity" }, { label: "At the very bottom", value: "bottom" }, - ] + ], }, }, get electronVersion() { - return VencordNative.native.getVersions().electron || window.legcord?.electron || null; + return ( + VencordNative.native.getVersions().electron || + window.legcord?.electron || + null + ); }, get chromiumVersion() { try { - return VencordNative.native.getVersions().chrome + return ( + VencordNative.native.getVersions().chrome || // @ts-expect-error Typescript will add userAgentData IMMEDIATELY - || navigator.userAgentData?.brands?.find(b => b.brand === "Chromium" || b.brand === "Google Chrome")?.version - || null; + navigator.userAgentData?.brands?.find( + b => + b.brand === "Chromium" || b.brand === "Google Chrome", + )?.version || + null + ); } catch { // inb4 some stupid browser throws unsupported error for navigator.userAgentData, it's only in chromium return null; @@ -258,9 +297,14 @@ export default definePlugin({ return "\n" + this.getInfoRows().join("\n"); }, - makeInfoElements(Component: React.ComponentType, props: React.PropsWithChildren) { - return this.getInfoRows().map((text, i) => - {text} - ); - } + makeInfoElements( + Component: React.ComponentType, + props: React.PropsWithChildren, + ) { + return this.getInfoRows().map((text, i) => ( + + {text} + + )); + }, });