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} + + )); + }, });