Merge branch 'dev'
This commit is contained in:
commit
5dd9a96439
11 changed files with 1934 additions and 407 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>
|
||||
));
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
makeSubtext(user: User, origSubtext: any) {
|
||||
const since = this.getSince(user);
|
||||
const since = getSince(user);
|
||||
if (isNaN(since.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue