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 <Jake0300@protonmai.com>
This commit is contained in:
parent
3d4e06ef79
commit
01c1484c8d
10 changed files with 1933 additions and 406 deletions
279
src/components/settings/tabs/changelog/NewPluginsSection.tsx
Normal file
279
src/components/settings/tabs/changelog/NewPluginsSection.tsx
Normal file
|
|
@ -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<string>(), []);
|
||||
const forceUpdate = useForceUpdater();
|
||||
|
||||
const depMap = React.useMemo(() => {
|
||||
const o = {} as Record<string, string[]>;
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||
{deps.map((dep: string) => (
|
||||
<Forms.FormText key={dep} className="vc-changelog-dep-text">
|
||||
{dep}
|
||||
</Forms.FormText>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cl("new-plugins-section")}>
|
||||
<Forms.FormTitle tag="h5" className={Margins.bottom8}>
|
||||
New Plugins ({sortedPlugins.length})
|
||||
</Forms.FormTitle>
|
||||
|
||||
<Forms.FormText className={Margins.bottom16}>
|
||||
The following plugins have been added in recent updates:
|
||||
</Forms.FormText>
|
||||
|
||||
<div className={cl("new-plugins-grid")}>
|
||||
{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 (
|
||||
<Tooltip text={tooltipText} key={plugin.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<Card
|
||||
className={cl(
|
||||
"new-plugin-card",
|
||||
"required",
|
||||
)}
|
||||
>
|
||||
<PluginCard
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onRestartNeeded={name => {
|
||||
changes.handleChange(name);
|
||||
forceUpdate();
|
||||
}}
|
||||
disabled={true}
|
||||
plugin={plugin}
|
||||
isNew={true}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={plugin.name}
|
||||
className={cl("new-plugin-card")}
|
||||
>
|
||||
<PluginCard
|
||||
onRestartNeeded={name => {
|
||||
changes.handleChange(name);
|
||||
forceUpdate();
|
||||
}}
|
||||
disabled={false}
|
||||
plugin={plugin}
|
||||
isNew={true}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{changes.hasChanges && (
|
||||
<div className={cl("restart-notice")}>
|
||||
<Tooltip
|
||||
text={
|
||||
<>
|
||||
The following plugins require a restart:
|
||||
<div className={Margins.bottom8} />
|
||||
<ul>
|
||||
{changes.map(p => (
|
||||
<li key={p}>{p}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{tooltipProps => (
|
||||
<Button
|
||||
{...tooltipProps}
|
||||
color={Button.Colors.YELLOW}
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={() => location.reload()}
|
||||
className={Margins.top16}
|
||||
>
|
||||
Restart Required
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface NewPluginsCompactProps {
|
||||
newPlugins: string[];
|
||||
maxDisplay?: number;
|
||||
}
|
||||
|
||||
function CompactPluginCard({
|
||||
pluginName,
|
||||
depMap,
|
||||
settings,
|
||||
}: {
|
||||
pluginName: string;
|
||||
depMap: Record<string, string[]>;
|
||||
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 (
|
||||
<div className={`vc-changelog-entry ${isRequired ? "required" : ""}`}>
|
||||
<div className="vc-changelog-entry-header">
|
||||
<span className="vc-changelog-entry-hash">
|
||||
{plugin.name}
|
||||
{isRequired && " *"}
|
||||
</span>
|
||||
<span className="vc-changelog-entry-author">
|
||||
{plugin.authors?.[0]?.name || "Unknown"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="vc-changelog-entry-message">
|
||||
{plugin.description || "No description available"}
|
||||
</div>
|
||||
{tooltipText && (
|
||||
<div className="vc-changelog-dep-text">{tooltipText}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewPluginsCompact({
|
||||
newPlugins,
|
||||
maxDisplay = 20,
|
||||
}: NewPluginsCompactProps) {
|
||||
const settings = useSettings();
|
||||
|
||||
const depMap = React.useMemo(() => {
|
||||
const o = {} as Record<string, string[]>;
|
||||
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 (
|
||||
<div className={cl("new-plugins-compact")}>
|
||||
<div className="vc-changelog-plugins-list">
|
||||
{displayPlugins.map(pluginName => (
|
||||
<CompactPluginCard
|
||||
key={pluginName}
|
||||
pluginName={pluginName}
|
||||
depMap={depMap}
|
||||
settings={settings}
|
||||
/>
|
||||
))}
|
||||
|
||||
{hasMore && (
|
||||
<div className="vc-changelog-entry">
|
||||
<div className="vc-changelog-entry-message">
|
||||
+{newPlugins.length - maxDisplay} more plugins
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
282
src/components/settings/tabs/changelog/changelogManager.ts
Normal file
282
src/components/settings/tabs/changelog/changelogManager.ts
Normal file
|
|
@ -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<string, string[]>;
|
||||
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<string, Set<string>>;
|
||||
|
||||
export async function getChangelogHistory(): Promise<ChangelogHistory> {
|
||||
const history = (await DataStore.get(
|
||||
CHANGELOG_HISTORY_KEY,
|
||||
)) as ChangelogHistory;
|
||||
return history || [];
|
||||
}
|
||||
|
||||
export async function saveUpdateSession(
|
||||
commits: ChangelogEntry[],
|
||||
newPlugins: string[],
|
||||
updatedPlugins: string[],
|
||||
newSettings: Map<string, string[]>,
|
||||
forceLog: boolean = false,
|
||||
): Promise<void> {
|
||||
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<string | null> {
|
||||
return (await DataStore.get(LAST_SEEN_HASH_KEY)) as string | null;
|
||||
}
|
||||
|
||||
export async function setLastSeenHash(hash: string): Promise<void> {
|
||||
await DataStore.set(LAST_SEEN_HASH_KEY, hash);
|
||||
}
|
||||
|
||||
export async function getKnownPlugins(): Promise<Set<string>> {
|
||||
const known = (await DataStore.get(KNOWN_PLUGINS_KEY)) as string[];
|
||||
return new Set(known || []);
|
||||
}
|
||||
|
||||
export async function updateKnownPlugins(): Promise<void> {
|
||||
const currentPlugins = Object.keys(plugins);
|
||||
await DataStore.set(KNOWN_PLUGINS_KEY, currentPlugins);
|
||||
}
|
||||
|
||||
function getSettingsSetForPlugin(plugin: string): Set<string> {
|
||||
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<KnownPluginSettingsMap> {
|
||||
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<Map<string, string[]>> {
|
||||
const map = getCurrentSettings(Object.keys(plugins));
|
||||
const knownSettings = await getKnownSettings();
|
||||
const newSettings = new Map<string, string[]>();
|
||||
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<string[]> {
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
const history = await getChangelogHistory();
|
||||
const filteredHistory = history.filter(log => log.id !== logId);
|
||||
await DataStore.set(CHANGELOG_HISTORY_KEY, filteredHistory);
|
||||
}
|
||||
|
||||
export async function initializeChangelog(): Promise<void> {
|
||||
// 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<string | null> {
|
||||
return (await DataStore.get(LAST_REPO_CHECK_KEY)) as string | null;
|
||||
}
|
||||
|
||||
export async function setLastRepositoryCheckHash(hash: string): Promise<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
693
src/components/settings/tabs/changelog/index.tsx
Normal file
693
src/components/settings/tabs/changelog/index.tsx
Normal file
|
|
@ -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 (
|
||||
<Link href={`${repo}/commit/${hash}`} disabled={disabled}>
|
||||
{hash}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangelogCard({
|
||||
entry,
|
||||
repo,
|
||||
repoPending,
|
||||
}: {
|
||||
entry: ChangelogEntry;
|
||||
repo: string;
|
||||
repoPending: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card className="vc-changelog-entry">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.25em",
|
||||
}}
|
||||
>
|
||||
<div className="vc-changelog-entry-header">
|
||||
<code className="vc-changelog-entry-hash">
|
||||
<HashLink
|
||||
repo={repo}
|
||||
hash={entry.hash.slice(0, 7)}
|
||||
disabled={repoPending}
|
||||
/>
|
||||
</code>
|
||||
<span className="vc-changelog-entry-author">
|
||||
by {entry.author}
|
||||
</span>
|
||||
</div>
|
||||
<div className="vc-changelog-entry-message">
|
||||
{entry.message}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card className="vc-changelog-log">
|
||||
<div className="vc-changelog-log-header" onClick={onToggleExpand}>
|
||||
<div className="vc-changelog-log-info">
|
||||
<div className="vc-changelog-log-title">
|
||||
<span>
|
||||
{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)}`}
|
||||
</span>
|
||||
<Button
|
||||
size={Button.Sizes.NONE}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.BLANK}
|
||||
className="vc-changelog-delete-button"
|
||||
style={{
|
||||
padding: "4px",
|
||||
color: "var(--status-danger)",
|
||||
opacity: 0.6,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClearLog(log.id);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="vc-changelog-log-meta">
|
||||
{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`}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`vc-changelog-log-toggle ${isExpanded ? "expanded" : ""}`}
|
||||
>
|
||||
▼
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="vc-changelog-log-content">
|
||||
{log.newPlugins.length > 0 && (
|
||||
<div className="vc-changelog-log-plugins">
|
||||
<NewPluginsCompact
|
||||
newPlugins={log.newPlugins}
|
||||
maxDisplay={50}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.updatedPlugins.length > 0 && (
|
||||
<div className="vc-changelog-log-plugins">
|
||||
<Forms.FormTitle
|
||||
tag="h6"
|
||||
className={Margins.bottom8}
|
||||
>
|
||||
Updated Plugins
|
||||
</Forms.FormTitle>
|
||||
<NewPluginsCompact
|
||||
newPlugins={log.updatedPlugins}
|
||||
maxDisplay={50}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.newSettings && log.newSettings.size > 0 && (
|
||||
<div className="vc-changelog-log-plugins">
|
||||
<Forms.FormTitle
|
||||
tag="h6"
|
||||
className={Margins.bottom8}
|
||||
>
|
||||
New Settings
|
||||
</Forms.FormTitle>
|
||||
<div className="vc-changelog-new-plugins-list">
|
||||
{Array.from(
|
||||
log.newSettings?.entries() || [],
|
||||
).map(([pluginName, settings]) =>
|
||||
settings.map(setting => (
|
||||
<span
|
||||
key={`${pluginName}-${setting}`}
|
||||
className="vc-changelog-new-plugin-tag"
|
||||
title={`New setting in ${pluginName}`}
|
||||
>
|
||||
{pluginName}.{setting}
|
||||
</span>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.commits.length > 0 && (
|
||||
<div className="vc-changelog-log-commits">
|
||||
<div className="vc-changelog-log-commits-list">
|
||||
{log.commits.map(entry => (
|
||||
<ChangelogCard
|
||||
key={entry.hash}
|
||||
entry={entry}
|
||||
repo={repo}
|
||||
repoPending={repoPending}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangelogContent() {
|
||||
const [repo, repoErr, repoPending] = useAwaiter(getRepo, {
|
||||
fallbackValue: "Loading...",
|
||||
});
|
||||
const [changelog, setChangelog] = React.useState<ChangelogEntry[]>([]);
|
||||
const [changelogHistory, setChangelogHistory] =
|
||||
React.useState<ChangelogHistory>([]);
|
||||
const [newPlugins, setNewPlugins] = React.useState<string[]>([]);
|
||||
const [updatedPlugins, setUpdatedPlugins] = React.useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [expandedLogs, setExpandedLogs] = React.useState<Set<string>>(
|
||||
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 (
|
||||
<>
|
||||
<Forms.FormText className={Margins.bottom16}>
|
||||
View the most recent changes to Equicord. This fetches commits
|
||||
directly from the repository to show you what's new.
|
||||
</Forms.FormText>
|
||||
|
||||
<Forms.FormTitle tag="h5">Repository</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>
|
||||
{repoPending ? (
|
||||
repo
|
||||
) : repoErr ? (
|
||||
"Failed to retrieve - check console"
|
||||
) : (
|
||||
<Link href={repo}>
|
||||
{repo.split("/").slice(-2).join("/")}
|
||||
</Link>
|
||||
)}{" "}
|
||||
(Current:{" "}
|
||||
<span className="vc-changelog-current-hash">
|
||||
{shortGitHash()}
|
||||
</span>
|
||||
)
|
||||
</Forms.FormText>
|
||||
|
||||
<div className="vc-changelog-controls">
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={isLoading || repoPending || !!repoErr}
|
||||
onClick={fetchChangelog}
|
||||
color={
|
||||
recentlyChecked
|
||||
? Button.Colors.GREEN
|
||||
: Button.Colors.BRAND
|
||||
}
|
||||
>
|
||||
{isLoading
|
||||
? "Loading..."
|
||||
: recentlyChecked
|
||||
? "Repository Up to Date"
|
||||
: "Fetch from Repository"}
|
||||
</Button>
|
||||
|
||||
{changelogHistory.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={
|
||||
showHistory
|
||||
? Button.Colors.PRIMARY
|
||||
: Button.Colors.BRAND
|
||||
}
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
style={{ marginLeft: "8px" }}
|
||||
>
|
||||
{showHistory ? "Hide Logs" : "Show Logs"}
|
||||
</Button>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
Alerts.show({
|
||||
title: "Clear All Logs",
|
||||
body: "Are you sure you would like to clear all logs? This can't be undone.",
|
||||
confirmText: "Clear All",
|
||||
confirmColor: "danger",
|
||||
cancelText: "Cancel",
|
||||
onConfirm: async () => {
|
||||
await clearChangelogHistory();
|
||||
await loadChangelogHistory();
|
||||
setShowHistory(false);
|
||||
Toasts.show({
|
||||
message:
|
||||
"All logs have been cleared",
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.SUCCESS,
|
||||
options: {
|
||||
position:
|
||||
Toasts.Position.BOTTOM,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{ marginLeft: "8px" }}
|
||||
>
|
||||
Clear All Logs
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<ErrorCard style={{ padding: "1em", marginBottom: "1em" }}>
|
||||
<p>{error}</p>
|
||||
<Forms.FormText
|
||||
style={{
|
||||
marginTop: "0.5em",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
Make sure you have an internet connection and try again.
|
||||
</Forms.FormText>
|
||||
</ErrorCard>
|
||||
)}
|
||||
|
||||
<Forms.FormDivider className={Margins.bottom16} />
|
||||
|
||||
{/* Current Changes Section */}
|
||||
{hasCurrentChanges ? (
|
||||
<div className="vc-changelog-current">
|
||||
<Forms.FormTitle tag="h6" className={Margins.bottom8}>
|
||||
Recent Changes
|
||||
</Forms.FormTitle>
|
||||
|
||||
{/* New Plugins Section */}
|
||||
{newPlugins.length > 0 && (
|
||||
<div className={Margins.bottom16}>
|
||||
<NewPluginsSection
|
||||
newPlugins={newPlugins}
|
||||
onPluginToggle={(pluginName, enabled) => {
|
||||
// Handle plugin toggle if needed
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Updated Plugins Section */}
|
||||
{updatedPlugins.length > 0 && (
|
||||
<div className={Margins.bottom16}>
|
||||
<Forms.FormTitle
|
||||
tag="h6"
|
||||
className={Margins.bottom8}
|
||||
>
|
||||
Updated Plugins ({updatedPlugins.length})
|
||||
</Forms.FormTitle>
|
||||
<NewPluginsCompact newPlugins={updatedPlugins} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Code Changes */}
|
||||
{changelog.length > 0 && (
|
||||
<div>
|
||||
<Forms.FormTitle
|
||||
tag="h6"
|
||||
className={Margins.bottom8}
|
||||
>
|
||||
Code Changes ({changelog.length}{" "}
|
||||
{changelog.length === 1 ? "commit" : "commits"})
|
||||
</Forms.FormTitle>
|
||||
<div className="vc-changelog-commits-list">
|
||||
{changelog.map(entry => (
|
||||
<ChangelogCard
|
||||
key={entry.hash}
|
||||
entry={entry}
|
||||
repo={repo}
|
||||
repoPending={repoPending}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
!isLoading &&
|
||||
!error && (
|
||||
<Card className="vc-changelog-empty">
|
||||
<Forms.FormText>
|
||||
No commits available ahead of your current version.
|
||||
Click "Fetch from Repository" to check for new
|
||||
changes.
|
||||
</Forms.FormText>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* History Section */}
|
||||
{showHistory && changelogHistory.length > 0 && (
|
||||
<div className="vc-changelog-history">
|
||||
<Forms.FormDivider
|
||||
className={Margins.top16}
|
||||
style={{ marginBottom: "1em" }}
|
||||
/>
|
||||
<Forms.FormTitle tag="h5" className={Margins.bottom8}>
|
||||
Update Logs ({changelogHistory.length}{" "}
|
||||
{changelogHistory.length === 1 ? "log" : "logs"})
|
||||
</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>
|
||||
Previous update sessions with their commit history and
|
||||
plugin changes.
|
||||
</Forms.FormText>
|
||||
|
||||
<div className="vc-changelog-history-list">
|
||||
{changelogHistory.map(log => (
|
||||
<UpdateLogCard
|
||||
key={log.id}
|
||||
log={log}
|
||||
repo={repo}
|
||||
repoPending={repoPending}
|
||||
isExpanded={expandedLogs.has(log.id)}
|
||||
onToggleExpand={() => 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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangelogTab() {
|
||||
return (
|
||||
<SettingsTab title="Changelog">
|
||||
<ChangelogContent />
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(ChangelogTab, "Changelog");
|
||||
572
src/components/settings/tabs/changelog/styles.css
Normal file
572
src/components/settings/tabs/changelog/styles.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<string>; newSettings: KnownPluginSettingsMap; }) {
|
||||
const settings = useSettings();
|
||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||
let updateContinueButton = () => { };
|
||||
|
||||
const depMap = React.useMemo(() => {
|
||||
const o = {} as Record<string, string[]>;
|
||||
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(
|
||||
<Tooltip text={tooltipText} key={p.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<PluginCard
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onRestartNeeded={name => {
|
||||
changes.handleChange(name);
|
||||
updateContinueButton();
|
||||
}}
|
||||
disabled={true}
|
||||
plugin={p}
|
||||
key={p.name}
|
||||
isNew={newPlugins.has(p.name)}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
plugins.push(
|
||||
<PluginCard
|
||||
onRestartNeeded={name => {
|
||||
changes.handleChange(name);
|
||||
updateContinueButton();
|
||||
}}
|
||||
disabled={false}
|
||||
plugin={p}
|
||||
key={p.name}
|
||||
isNew={newPlugins.has(p.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <ModalRoot {...modalProps} size={ModalSize.MEDIUM} >
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>New Plugins and Settings ({[...plugins, ...requiredPlugins].length})</Text>
|
||||
<Tooltip text="Dismiss for this session">
|
||||
{tooltipProps =>
|
||||
<div {...tooltipProps}>
|
||||
<ModalCloseButton
|
||||
onClick={modalProps.onClose}
|
||||
className={classes(cl("close"), "vc-newPluginsManager-close")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</Tooltip>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div className={cl("grid")}>
|
||||
{[...plugins, ...requiredPlugins]}
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Flex direction={Flex.Direction.HORIZONTAL_REVERSE}>
|
||||
<ContinueButton
|
||||
close={modalProps.onClose}
|
||||
changes={changes}
|
||||
callback={(v: () => void) => updateContinueButton = v}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex direction={Flex.Direction.HORIZONTAL}>
|
||||
<div className="vc-newPluginsManager-disable-wrapper">
|
||||
<Checkbox
|
||||
type="inverted"
|
||||
value={!settings?.plugins?.NewPluginsManager?.enabled}
|
||||
onChange={() => {
|
||||
Settings.plugins.NewPluginsManager.enabled = !settings?.plugins?.NewPluginsManager?.enabled;
|
||||
}}
|
||||
>
|
||||
<Text>Don't show this again</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalRoot>;
|
||||
}
|
||||
|
||||
function ContinueButton(props: { callback: (update: () => void) => void; changes: ChangeList<string>; close: () => any; }) {
|
||||
const update = useForceUpdater();
|
||||
props.callback(update);
|
||||
return <Tooltip
|
||||
tooltipClassName="vc-newPluginsManager-restart-tooltip"
|
||||
text={<>
|
||||
The following plugins require a restart:
|
||||
<div className={Margins.bottom8} />
|
||||
<ul className="vc-newPluginsManager-restart-list">
|
||||
{props.changes.map(p => <li key={p}>{p}</li>)}
|
||||
</ul>
|
||||
</>}
|
||||
shouldShow={props.changes.hasChanges}
|
||||
>
|
||||
{tooltipProps =>
|
||||
<Button
|
||||
{...tooltipProps}
|
||||
color={Button.Colors.GREEN}
|
||||
onClick={async () => {
|
||||
await writeKnownSettings();
|
||||
props.changes.hasChanges ? location.reload() : props.close();
|
||||
}}
|
||||
>
|
||||
{props.changes.hasChanges ? "Restart" : "Continue"}
|
||||
</Button>
|
||||
}
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
|
||||
function makeDependencyList(deps: string[]) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export async function openNewPluginsModal() {
|
||||
const newPlugins = await getNewPlugins();
|
||||
const newSettings = await getNewSettings();
|
||||
if ((newPlugins.size || newSettings.size) && !hasSeen) {
|
||||
hasSeen = true;
|
||||
const modalKey = openModal(modalProps => <ErrorBoundary noop onError={() => { closeModal(modalKey); }}>
|
||||
<NewPluginsModal
|
||||
modalProps={modalProps}
|
||||
newPlugins={newPlugins}
|
||||
newSettings={newSettings}
|
||||
/>
|
||||
</ErrorBoundary>);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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<string, Set<string>>;
|
||||
|
||||
export const KNOWN_PLUGINS_LEGACY_DATA_KEY = "NewPluginsManager_KnownPlugins";
|
||||
export const KNOWN_SETTINGS_DATA_KEY = "NewPluginsManager_KnownSettings";
|
||||
|
||||
function getSettingsSetForPlugin(plugin: string): Set<string> {
|
||||
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<KnownPluginSettingsMap> {
|
||||
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<KnownPluginSettingsMap> {
|
||||
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<Set<string>> {
|
||||
const knownSettings = await getKnownSettings();
|
||||
return new Set(knownSettings.keys());
|
||||
}
|
||||
|
||||
export async function getNewPlugins(): Promise<Set<string>> {
|
||||
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> | 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);
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* 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<SectionType, SectionType>;
|
||||
|
||||
|
|
@ -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<string, unknown>[]) {
|
||||
wrapSettingsHook(
|
||||
originalHook: (...args: any[]) => Record<string, unknown>[],
|
||||
) {
|
||||
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<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
||||
return this.getInfoRows().map((text, i) =>
|
||||
<Component key={i} {...props}>{text}</Component>
|
||||
);
|
||||
}
|
||||
makeInfoElements(
|
||||
Component: React.ComponentType<React.PropsWithChildren>,
|
||||
props: React.PropsWithChildren,
|
||||
) {
|
||||
return this.getInfoRows().map((text, i) => (
|
||||
<Component key={i} {...props}>
|
||||
{text}
|
||||
</Component>
|
||||
));
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue