Add in client updater, Notices API
This commit is contained in:
parent
9aaa47ea4e
commit
8161a07dba
|
@ -1,12 +1,41 @@
|
||||||
export * as Plugins from "./plugins";
|
export * as Plugins from "./plugins";
|
||||||
export * as Webpack from "./webpack";
|
export * as Webpack from "./webpack";
|
||||||
export * as Api from "./api";
|
export * as Api from "./api";
|
||||||
export { Settings } from "./api/settings";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
|
import { Settings } from "./api/settings";
|
||||||
|
import { startAllPlugins } from "./plugins";
|
||||||
|
|
||||||
|
export { Settings };
|
||||||
|
|
||||||
import "./utils/patchWebpack";
|
import "./utils/patchWebpack";
|
||||||
import "./utils/quickCss";
|
import "./utils/quickCss";
|
||||||
import { waitFor } from "./webpack";
|
import { checkForUpdates, UpdateLogger } from './utils/updater';
|
||||||
|
import { onceReady } from "./webpack";
|
||||||
|
import { Router } from "./webpack/common";
|
||||||
|
|
||||||
export let Components;
|
export let Components;
|
||||||
|
|
||||||
waitFor("useState", () => setTimeout(() => import("./components").then(mod => Components = mod), 0));
|
async function init() {
|
||||||
|
await onceReady;
|
||||||
|
startAllPlugins();
|
||||||
|
Components = await import("./components");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isOutdated = await checkForUpdates();
|
||||||
|
if (isOutdated && Settings.notifyAboutUpdates)
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotice(
|
||||||
|
"A Vencord update is available!",
|
||||||
|
"View Update",
|
||||||
|
() => {
|
||||||
|
popNotice();
|
||||||
|
Router.open("Vencord");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, 10000);
|
||||||
|
} catch (err) {
|
||||||
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
24
src/api/Notices.ts
Normal file
24
src/api/Notices.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { waitFor } from "../webpack";
|
||||||
|
|
||||||
|
let NoticesModule: any;
|
||||||
|
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
||||||
|
|
||||||
|
export const noticesQueue = [] as any[];
|
||||||
|
export let currentNotice: any = null;
|
||||||
|
|
||||||
|
export function popNotice() {
|
||||||
|
NoticesModule.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextNotice() {
|
||||||
|
currentNotice = noticesQueue.shift();
|
||||||
|
|
||||||
|
if (currentNotice) {
|
||||||
|
NoticesModule.show(...currentNotice, "VencordNotice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showNotice(message: string, buttonText: string, onOkClick: () => void) {
|
||||||
|
noticesQueue.push(["GENERIC", message, buttonText, onOkClick]);
|
||||||
|
if (!currentNotice) nextNotice();
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
export * as MessageEvents from "./MessageEvents";
|
export * as MessageEvents from "./MessageEvents";
|
||||||
|
export * as Notices from "./Notices";
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { React } from "../webpack/common";
|
||||||
import { mergeDefaults } from '../utils/misc';
|
import { mergeDefaults } from '../utils/misc';
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
|
notifyAboutUpdates: boolean;
|
||||||
unsafeRequire: boolean;
|
unsafeRequire: boolean;
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
plugins: {
|
plugins: {
|
||||||
|
@ -15,10 +16,11 @@ interface Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
|
notifyAboutUpdates: true,
|
||||||
unsafeRequire: false,
|
unsafeRequire: false,
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
plugins: {}
|
plugins: {}
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
for (const plugin in plugins) {
|
for (const plugin in plugins) {
|
||||||
DefaultSettings.plugins[plugin] = {
|
DefaultSettings.plugins[plugin] = {
|
||||||
|
@ -77,7 +79,7 @@ export const Settings = makeProxy(settings);
|
||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
const [, forceUpdate] = React.useReducer(x => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.add(forceUpdate);
|
subscriptions.add(forceUpdate);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Logger from "../utils/logger";
|
import Logger from "../utils/logger";
|
||||||
import { React } from "../webpack/common";
|
import { Card, React } from "../webpack/common";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>;
|
||||||
|
@ -16,7 +16,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
|
||||||
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
|
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
|
||||||
return (props) => (
|
return (props) => (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Component {...props} />
|
<Component {...props as any/* I hate react typings ??? */} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<Card style={{
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
padding: "2em",
|
padding: "2em",
|
||||||
backgroundColor: color + "30",
|
backgroundColor: color + "30",
|
||||||
|
@ -65,7 +65,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
|
||||||
<pre>{this.state.error}
|
<pre>{this.state.error}
|
||||||
</pre>
|
</pre>
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { React } from '../webpack/common';
|
||||||
export function Flex(props: React.PropsWithChildren<{
|
export function Flex(props: React.PropsWithChildren<{
|
||||||
flexDirection?: React.CSSProperties["flexDirection"];
|
flexDirection?: React.CSSProperties["flexDirection"];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
}>) {
|
}>) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.flexDirection ||= props.flexDirection;
|
props.style.flexDirection ||= props.flexDirection;
|
||||||
|
|
19
src/components/Link.tsx
Normal file
19
src/components/Link.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
href: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link(props: React.PropsWithChildren<Props>) {
|
||||||
|
if (props.disabled) {
|
||||||
|
props.style ??= {};
|
||||||
|
props.style.pointerEvents = "none";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a href={props.href} target="_blank" style={props.style}>
|
||||||
|
{props.children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,16 +1,19 @@
|
||||||
import { humanFriendlyJoin, useAwaiter } from "../utils/misc";
|
import { classes, humanFriendlyJoin, lazy, useAwaiter } from "../utils/misc";
|
||||||
import Plugins from 'plugins';
|
import Plugins from 'plugins';
|
||||||
import { useSettings } from "../api/settings";
|
import { useSettings } from "../api/settings";
|
||||||
import IpcEvents from "../utils/IpcEvents";
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
|
|
||||||
import { Button, Switch, Forms, React } from "../webpack/common";
|
import { Button, Switch, Forms, React, Margins } from "../webpack/common";
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
import { startPlugin } from "../plugins";
|
import { startPlugin } from "../plugins";
|
||||||
import { stopPlugin } from '../plugins/index';
|
import { stopPlugin } from '../plugins/index';
|
||||||
import { Flex } from './Flex';
|
import { Flex } from './Flex';
|
||||||
|
import { isOutdated } from "../utils/updater";
|
||||||
|
import { Updater } from "./Updater";
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(function Settings(props) {
|
export default ErrorBoundary.wrap(function Settings(props) {
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
||||||
|
const [outdated, setOutdated] = React.useState(isOutdated);
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
const depMap = React.useMemo(() => {
|
const depMap = React.useMemo(() => {
|
||||||
|
@ -31,8 +34,24 @@ export default ErrorBoundary.wrap(function Settings(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection tag="h1" title="Vencord">
|
<Forms.FormSection tag="h1" title="Vencord">
|
||||||
<Forms.FormText>SettingsDir: {settingsDir}</Forms.FormText>
|
{outdated && (
|
||||||
<Flex style={{ marginTop: "8px", marginBottom: "8px" }}>
|
<>
|
||||||
|
<Forms.FormTitle tag="h5">Updater</Forms.FormTitle>
|
||||||
|
<Updater setIsOutdated={setOutdated} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Forms.FormDivider />
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={outdated ? `${Margins.marginTop20} ${Margins.marginBottom8}` : ""}>
|
||||||
|
Settings
|
||||||
|
</Forms.FormTitle>
|
||||||
|
|
||||||
|
<Forms.FormText>
|
||||||
|
SettingsDir: {settingsDir}
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
|
<Flex className={classes(Margins.marginBottom20)}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir)}
|
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir)}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
|
@ -48,7 +67,7 @@ export default ErrorBoundary.wrap(function Settings(props) {
|
||||||
Open QuickCSS File
|
Open QuickCSS File
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Forms.FormTitle tag="h5">Settings</Forms.FormTitle>
|
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.useQuickCss}
|
value={settings.useQuickCss}
|
||||||
onChange={v => settings.useQuickCss = v}
|
onChange={v => settings.useQuickCss = v}
|
||||||
|
@ -56,6 +75,13 @@ export default ErrorBoundary.wrap(function Settings(props) {
|
||||||
>
|
>
|
||||||
Use QuickCss
|
Use QuickCss
|
||||||
</Switch>
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
value={settings.notifyAboutUpdates}
|
||||||
|
onChange={v => settings.notifyAboutUpdates = v}
|
||||||
|
note="Shows a Toast on StartUp"
|
||||||
|
>
|
||||||
|
Get notified about new Updates
|
||||||
|
</Switch>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.unsafeRequire}
|
value={settings.unsafeRequire}
|
||||||
onChange={v => settings.unsafeRequire = v}
|
onChange={v => settings.unsafeRequire = v}
|
||||||
|
@ -63,8 +89,13 @@ export default ErrorBoundary.wrap(function Settings(props) {
|
||||||
>
|
>
|
||||||
Enable Unsafe Require
|
Enable Unsafe Require
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
<Forms.FormDivider />
|
<Forms.FormDivider />
|
||||||
<Forms.FormTitle tag="h5">Plugins</Forms.FormTitle>
|
|
||||||
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
|
Plugins
|
||||||
|
</Forms.FormTitle>
|
||||||
|
|
||||||
{sortedPlugins.map(p => {
|
{sortedPlugins.map(p => {
|
||||||
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
|
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
const dependency = enabledDependants?.length;
|
const dependency = enabledDependants?.length;
|
||||||
|
|
128
src/components/Updater.tsx
Normal file
128
src/components/Updater.tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import gitHash from "git-hash";
|
||||||
|
import { changes, checkForUpdates, getRepo, rebuild, update, UpdateLogger } from "../utils/updater";
|
||||||
|
import { React, Forms, Button, Margins, Alerts, Card, Parser } from '../webpack/common';
|
||||||
|
import { Flex } from "./Flex";
|
||||||
|
import { useAwaiter } from '../utils/misc';
|
||||||
|
import { Link } from "./Link";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
setIsOutdated(b: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||||
|
return async () => {
|
||||||
|
dispatcher(true);
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} catch (e: any) {
|
||||||
|
UpdateLogger.error("Failed to update", e);
|
||||||
|
if (!e) {
|
||||||
|
var err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||||
|
} else if (e.code && e.cmd) {
|
||||||
|
const { code, path, cmd, stderr } = e;
|
||||||
|
|
||||||
|
if (code === "ENOENT")
|
||||||
|
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||||
|
else {
|
||||||
|
var err = `An error occured while running \`${cmd}\`:\n`;
|
||||||
|
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
var err = "An unknown error occurred. See the console for more info.";
|
||||||
|
}
|
||||||
|
Alerts.show({
|
||||||
|
title: "Oops!",
|
||||||
|
body: err.split("\n").map(line => <div>{Parser.parse(line)}</div>)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
dispatcher(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Updater(p: Props) {
|
||||||
|
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
|
||||||
|
const [isChecking, setIsChecking] = React.useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||||
|
const [updates, setUpdates] = React.useState(changes);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (err)
|
||||||
|
UpdateLogger.error("Failed to retrieve repo", err);
|
||||||
|
}, [err]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormText>Repo: {repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||||
|
<Link href={repo}>
|
||||||
|
{repo.split("/").slice(-2).join("/")}
|
||||||
|
</Link>
|
||||||
|
)} ({gitHash})</Forms.FormText>
|
||||||
|
|
||||||
|
<Forms.FormText className={Margins.marginBottom8}>
|
||||||
|
There are {updates.length} Updates
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
|
<Card style={{ padding: ".5em" }}>
|
||||||
|
{updates.map(({ hash, author, message }) => (
|
||||||
|
<div>
|
||||||
|
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
|
||||||
|
<code>{hash}</code>
|
||||||
|
</Link>
|
||||||
|
<span style={{
|
||||||
|
marginLeft: "0.5em",
|
||||||
|
color: "var(--text-normal)"
|
||||||
|
}}>{message} - {author}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Flex className={`${Margins.marginBottom8} ${Margins.marginTop8}`}>
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={isUpdating || isChecking}
|
||||||
|
onClick={withDispatcher(setIsUpdating, async () => {
|
||||||
|
if (await update()) {
|
||||||
|
p.setIsOutdated(false);
|
||||||
|
const needFullRestart = await rebuild();
|
||||||
|
await new Promise<void>(r => {
|
||||||
|
Alerts.show({
|
||||||
|
title: "Update Success!",
|
||||||
|
body: "Successfully updated. Restart now to apply the changes?",
|
||||||
|
confirmText: "Restart",
|
||||||
|
cancelText: "Not now!",
|
||||||
|
onConfirm() {
|
||||||
|
if (needFullRestart)
|
||||||
|
window.DiscordNative.app.relaunch();
|
||||||
|
else
|
||||||
|
location.reload();
|
||||||
|
r();
|
||||||
|
},
|
||||||
|
onCancel: r
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={isUpdating || isChecking}
|
||||||
|
onClick={withDispatcher(setIsChecking, async () => {
|
||||||
|
const res = await checkForUpdates();
|
||||||
|
if (res) {
|
||||||
|
setUpdates(changes);
|
||||||
|
} else {
|
||||||
|
p.setIsOutdated(false);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,17 +1,50 @@
|
||||||
|
// TODO: refactor this mess
|
||||||
|
|
||||||
|
import { execFile as cpExecFile } from 'child_process';
|
||||||
|
import { createHash } from "crypto";
|
||||||
import { app, BrowserWindow, ipcMain, shell } from "electron";
|
import { app, BrowserWindow, ipcMain, shell } from "electron";
|
||||||
import { mkdirSync, readFileSync, watch } from "fs";
|
import { createReadStream, mkdirSync, readFileSync, watch } from "fs";
|
||||||
import { open, readFile, writeFile } from "fs/promises";
|
import { open, readFile, writeFile } from "fs/promises";
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { promisify } from "util";
|
||||||
import { debounce } from "./utils/debounce";
|
import { debounce } from "./utils/debounce";
|
||||||
import IpcEvents from './utils/IpcEvents';
|
import IpcEvents from './utils/IpcEvents';
|
||||||
|
|
||||||
|
const VENCORD_SRC_DIR = join(__dirname, "..");
|
||||||
const DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
const DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||||
const SETTINGS_DIR = join(DATA_DIR, "settings");
|
const SETTINGS_DIR = join(DATA_DIR, "settings");
|
||||||
const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
||||||
const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
||||||
|
|
||||||
|
const execFile = promisify(cpExecFile);
|
||||||
|
|
||||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
async function calculateHashes() {
|
||||||
|
const hashes = {} as Record<string, string>;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
|
||||||
|
const fis = createReadStream(join(__dirname, file));
|
||||||
|
const hash = createHash("sha1", { encoding: "hex" });
|
||||||
|
fis.once("end", () => {
|
||||||
|
hash.end();
|
||||||
|
hashes[file] = hash.read();
|
||||||
|
r();
|
||||||
|
});
|
||||||
|
fis.pipe(hash);
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function git(...args: string[]) {
|
||||||
|
return execFile("git", args, {
|
||||||
|
cwd: VENCORD_SRC_DIR
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function readCss() {
|
function readCss() {
|
||||||
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
|
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
|
||||||
}
|
}
|
||||||
|
@ -24,11 +57,65 @@ function readSettings() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializeErrors(func: (...args: any[]) => any) {
|
||||||
|
return async function () {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: await func(...arguments)
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: e instanceof Error ? {
|
||||||
|
// prototypes get lost, so turn error into plain object
|
||||||
|
...e
|
||||||
|
} : e
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||||
ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements)));
|
ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements)));
|
||||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url));
|
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url));
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(async () => {
|
||||||
|
await git("fetch");
|
||||||
|
|
||||||
|
const res = await git("log", `HEAD...origin/main`, "--pretty=format:%h-%s");
|
||||||
|
|
||||||
|
const commits = res.stdout.trim();
|
||||||
|
return commits ? commits.split("\n").map(line => {
|
||||||
|
const [author, hash, ...rest] = line.split("/");
|
||||||
|
return {
|
||||||
|
hash, author, message: rest.join("/")
|
||||||
|
};
|
||||||
|
}) : [];
|
||||||
|
}));
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(async () => {
|
||||||
|
const res = await git("pull");
|
||||||
|
return res.stdout.includes("Fast-forward");
|
||||||
|
}));
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.BUILD, serializeErrors(async () => {
|
||||||
|
const res = await execFile("node", ["build.mjs"], {
|
||||||
|
cwd: VENCORD_SRC_DIR
|
||||||
|
});
|
||||||
|
return !res.stderr.includes("Build failed");
|
||||||
|
}));
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(async () => {
|
||||||
|
const res = await git("remote", "get-url", "origin");
|
||||||
|
return res.stdout.trim()
|
||||||
|
.replace(/git@(.+):/, "https://$1/")
|
||||||
|
.replace(/\.git$/, "");
|
||||||
|
}));
|
||||||
|
|
||||||
// .on because we need Settings synchronously (ipcRenderer.sendSync)
|
// .on because we need Settings synchronously (ipcRenderer.sendSync)
|
||||||
ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings());
|
ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings());
|
||||||
|
|
||||||
|
|
24
src/plugins/apiNotices.ts
Normal file
24
src/plugins/apiNotices.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import definePlugin from "../utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ApiNotices",
|
||||||
|
description: "Fixes notices being automatically dismissed",
|
||||||
|
author: "Vendicated",
|
||||||
|
required: true,
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "updateNotice:",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /;(.{1,2}=null;)(?=.{0,50}updateNotice)/g,
|
||||||
|
replace:
|
||||||
|
';if(Vencord.Api.Notices.currentNotice)return !1;$1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/,
|
||||||
|
replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
|
@ -17,7 +17,7 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
|
|
||||||
copyToClipBoard(color: string) {
|
copyToClipBoard(color: string) {
|
||||||
DiscordNative.clipboard.copy(color);
|
window.DiscordNative.clipboard.copy(color);
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
message: "Copied to Clipboard!",
|
message: "Copied to Clipboard!",
|
||||||
type: Toasts.Type.SUCCESS,
|
type: Toasts.Type.SUCCESS,
|
||||||
|
|
|
@ -16,7 +16,7 @@ for (const plugin of Object.values(Plugins)) if (plugin.patches && Settings.plug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startAll() {
|
export function startAllPlugins() {
|
||||||
for (const plugin in Plugins) if (Settings.plugins[plugin].enabled) {
|
for (const plugin in Plugins) if (Settings.plugins[plugin].enabled) {
|
||||||
startPlugin(Plugins[plugin]);
|
startPlugin(Plugins[plugin]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,4 +19,9 @@ export default strEnum({
|
||||||
SET_SETTINGS: "VencordSetSettings",
|
SET_SETTINGS: "VencordSetSettings",
|
||||||
OPEN_EXTERNAL: "VencordOpenExternal",
|
OPEN_EXTERNAL: "VencordOpenExternal",
|
||||||
OPEN_PATH: "VencordOpenPath",
|
OPEN_PATH: "VencordOpenPath",
|
||||||
|
GET_UPDATES: "VencordGetUpdates",
|
||||||
|
GET_REPO: "VencordGetRepo",
|
||||||
|
GET_HASHES: "VencordGetHashes",
|
||||||
|
UPDATE: "VencordUpdate",
|
||||||
|
BUILD: "VencordBuild"
|
||||||
} as const);
|
} as const);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { FilterFn, find } from "../webpack";
|
||||||
import { React } from "../webpack/common";
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,9 +8,22 @@ import { React } from "../webpack/common";
|
||||||
*/
|
*/
|
||||||
export function lazy<T>(factory: () => T): () => T {
|
export function lazy<T>(factory: () => T): () => T {
|
||||||
let cache: T;
|
let cache: T;
|
||||||
return () => {
|
return () => cache ?? (cache = factory());
|
||||||
return cache ?? (cache = factory());
|
}
|
||||||
};
|
|
||||||
|
/**
|
||||||
|
* Do a lazy webpack search. Searches the module on first property access
|
||||||
|
* @param filter Filter function
|
||||||
|
* @returns Proxy. Note that only get and set are implemented, all other operations will have unexpected
|
||||||
|
* results.
|
||||||
|
*/
|
||||||
|
export function lazyWebpack<T = any>(filter: FilterFn): T {
|
||||||
|
const getMod = lazy(() => find(filter));
|
||||||
|
|
||||||
|
return new Proxy({}, {
|
||||||
|
get: (_, prop) => getMod()[prop],
|
||||||
|
set: (_, prop, v) => getMod()[prop] = v
|
||||||
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,7 +62,7 @@ export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T | null
|
||||||
export function LazyComponent<T = any>(factory: () => React.ComponentType<T>) {
|
export function LazyComponent<T = any>(factory: () => React.ComponentType<T>) {
|
||||||
return (props: T) => {
|
return (props: T) => {
|
||||||
const Component = React.useMemo(factory, []);
|
const Component = React.useMemo(factory, []);
|
||||||
return <Component {...props} />;
|
return <Component {...props as any /* I hate react typings ??? */} />;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,3 +112,11 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string =
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls .join(" ") on the arguments
|
||||||
|
* classes("one", "two") => "one two"
|
||||||
|
*/
|
||||||
|
export function classes(...classes: string[]) {
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
|
|
|
@ -29,3 +29,5 @@ interface PluginDef {
|
||||||
dependencies?: string[],
|
dependencies?: string[],
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };
|
||||||
|
|
51
src/utils/updater.ts
Normal file
51
src/utils/updater.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import IpcEvents from "./IpcEvents";
|
||||||
|
import Logger from "./logger";
|
||||||
|
import { IpcRes } from './types';
|
||||||
|
|
||||||
|
export const UpdateLogger = new Logger("Updater", "white");
|
||||||
|
export let isOutdated = false;
|
||||||
|
export let changes: Record<"hash" | "author" | "message", string>[];
|
||||||
|
|
||||||
|
async function Unwrap<T>(p: Promise<IpcRes<T>>) {
|
||||||
|
const res = await p;
|
||||||
|
|
||||||
|
if (res.ok) return res.value;
|
||||||
|
throw res.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkForUpdates() {
|
||||||
|
changes = await Unwrap(VencordNative.ipc.invoke<IpcRes<typeof changes>>(IpcEvents.GET_UPDATES));
|
||||||
|
return (isOutdated = changes.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update() {
|
||||||
|
if (!isOutdated) return true;
|
||||||
|
|
||||||
|
const res = await Unwrap(VencordNative.ipc.invoke<IpcRes<boolean>>(IpcEvents.UPDATE));
|
||||||
|
|
||||||
|
if (res)
|
||||||
|
isOutdated = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRepo() {
|
||||||
|
return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO));
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hashes = Record<"patcher.js" | "preload.js" | "renderer.js", string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if hard restart is required
|
||||||
|
*/
|
||||||
|
export async function rebuild() {
|
||||||
|
const oldHashes = await Unwrap(VencordNative.ipc.invoke<IpcRes<Hashes>>(IpcEvents.GET_HASHES));
|
||||||
|
|
||||||
|
if (!await Unwrap(VencordNative.ipc.invoke<IpcRes<boolean>>(IpcEvents.BUILD)))
|
||||||
|
throw new Error("The Build failed. Please try manually building the new update");
|
||||||
|
|
||||||
|
const newHashes = await Unwrap(VencordNative.ipc.invoke<IpcRes<Hashes>>(IpcEvents.GET_HASHES));
|
||||||
|
|
||||||
|
return oldHashes["patcher.js"] !== newHashes["patcher.js"] ||
|
||||||
|
oldHashes["preload.js"] !== newHashes["preload.js"];
|
||||||
|
}
|
|
@ -1,17 +1,43 @@
|
||||||
import { startAll } from "../plugins";
|
import { waitFor, filters, _resolveReady } from './webpack';
|
||||||
import { waitFor, filters, findByProps } from './webpack';
|
|
||||||
import type Components from "discord-types/components";
|
import type Components from "discord-types/components";
|
||||||
import type Stores from "discord-types/stores";
|
import type Stores from "discord-types/stores";
|
||||||
import type Other from "discord-types/other";
|
import type Other from "discord-types/other";
|
||||||
|
import { lazyWebpack } from '../utils/misc';
|
||||||
|
|
||||||
|
export const Margins = lazyWebpack(filters.byProps(["marginTop20"]));
|
||||||
|
|
||||||
export let FluxDispatcher: Other.FluxDispatcher;
|
export let FluxDispatcher: Other.FluxDispatcher;
|
||||||
export let React: typeof import("react");
|
export let React: typeof import("react");
|
||||||
export let UserStore: Stores.UserStore;
|
export let UserStore: Stores.UserStore;
|
||||||
export const Forms: any = {};
|
export const Forms = {} as {
|
||||||
|
FormTitle: Components.FormTitle;
|
||||||
|
FormSection: any;
|
||||||
|
FormDivider: any;
|
||||||
|
FormText: Components.FormText;
|
||||||
|
};
|
||||||
|
export let Card: Components.Card;
|
||||||
export let Button: any;
|
export let Button: any;
|
||||||
export let Switch: any;
|
export let Switch: any;
|
||||||
export let Tooltip: Components.Tooltip;
|
export let Tooltip: Components.Tooltip;
|
||||||
|
export let Router: any;
|
||||||
|
|
||||||
|
export let Parser: any;
|
||||||
|
export let Alerts: {
|
||||||
|
show(alert: {
|
||||||
|
title: any;
|
||||||
|
body: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
confirmColor?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
secondaryConfirmText?: string;
|
||||||
|
onCancel?(): void;
|
||||||
|
onConfirm?(): void;
|
||||||
|
onConfirmSecondary?(): void;
|
||||||
|
}): void;
|
||||||
|
/** This is a noop, it does nothing. */
|
||||||
|
close(): void;
|
||||||
|
};
|
||||||
const ToastType = {
|
const ToastType = {
|
||||||
MESSAGE: 0,
|
MESSAGE: 0,
|
||||||
SUCCESS: 1,
|
SUCCESS: 1,
|
||||||
|
@ -27,28 +53,28 @@ export const Toasts = {
|
||||||
Type: ToastType,
|
Type: ToastType,
|
||||||
Position: ToastPosition,
|
Position: ToastPosition,
|
||||||
// what's less likely than getting 0 from Math.random()? Getting it twice in a row
|
// what's less likely than getting 0 from Math.random()? Getting it twice in a row
|
||||||
genId: () => (Math.random() || Math.random()).toString(36).slice(2)
|
genId: () => (Math.random() || Math.random()).toString(36).slice(2),
|
||||||
} as {
|
|
||||||
Type: typeof ToastType,
|
// hack to merge with the following interface, dunno if there's a better way
|
||||||
Position: typeof ToastPosition;
|
...{} as {
|
||||||
genId(): string;
|
show(data: {
|
||||||
show(data: {
|
message: string,
|
||||||
message: string,
|
id: string,
|
||||||
id: string,
|
|
||||||
/**
|
|
||||||
* Toasts.Type
|
|
||||||
*/
|
|
||||||
type: number,
|
|
||||||
options?: {
|
|
||||||
/**
|
/**
|
||||||
* Toasts.Position
|
* Toasts.Type
|
||||||
*/
|
*/
|
||||||
position?: number;
|
type: number,
|
||||||
component?: React.ReactNode,
|
options?: {
|
||||||
duration?: number;
|
/**
|
||||||
};
|
* Toasts.Position
|
||||||
}): void;
|
*/
|
||||||
pop(): void;
|
position?: number;
|
||||||
|
component?: React.ReactNode,
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
}): void;
|
||||||
|
pop(): void;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
waitFor("useState", m => React = m);
|
waitFor("useState", m => React = m);
|
||||||
|
@ -56,7 +82,7 @@ waitFor(["dispatch", "subscribe"], m => {
|
||||||
FluxDispatcher = m;
|
FluxDispatcher = m;
|
||||||
const cb = () => {
|
const cb = () => {
|
||||||
m.unsubscribe("CONNECTION_OPEN", cb);
|
m.unsubscribe("CONNECTION_OPEN", cb);
|
||||||
startAll();
|
_resolveReady();
|
||||||
};
|
};
|
||||||
m.subscribe("CONNECTION_OPEN", cb);
|
m.subscribe("CONNECTION_OPEN", cb);
|
||||||
});
|
});
|
||||||
|
@ -64,6 +90,7 @@ waitFor(["getCurrentUser", "initialize"], m => UserStore = m);
|
||||||
waitFor(["Hovers", "Looks", "Sizes"], m => Button = m);
|
waitFor(["Hovers", "Looks", "Sizes"], m => Button = m);
|
||||||
waitFor(filters.byCode("helpdeskArticleId"), m => Switch = m);
|
waitFor(filters.byCode("helpdeskArticleId"), m => Switch = m);
|
||||||
waitFor(["Positions", "Colors"], m => Tooltip = m);
|
waitFor(["Positions", "Colors"], m => Tooltip = m);
|
||||||
|
waitFor(m => m.Types?.PRIMARY === "cardPrimary", m => Card = m);
|
||||||
|
|
||||||
waitFor(m => m.Tags && filters.byCode("errorSeparator")(m), m => Forms.FormTitle = m);
|
waitFor(m => m.Tags && filters.byCode("errorSeparator")(m), m => Forms.FormTitle = m);
|
||||||
waitFor(m => m.Tags && filters.byCode("titleClassName", "sectionTitle")(m), m => Forms.FormSection = m);
|
waitFor(m => m.Tags && filters.byCode("titleClassName", "sectionTitle")(m), m => Forms.FormSection = m);
|
||||||
|
@ -78,3 +105,8 @@ waitFor(m => {
|
||||||
// This is the same module but this is easier
|
// This is the same module but this is easier
|
||||||
waitFor(filters.byCode("currentToast?"), m => Toasts.show = m);
|
waitFor(filters.byCode("currentToast?"), m => Toasts.show = m);
|
||||||
waitFor(filters.byCode("currentToast:null"), m => Toasts.pop = m);
|
waitFor(filters.byCode("currentToast:null"), m => Toasts.pop = m);
|
||||||
|
|
||||||
|
waitFor(["show", "close"], m => Alerts = m);
|
||||||
|
waitFor("parseTopic", m => Parser = m);
|
||||||
|
|
||||||
|
waitFor(["open", "saveAccountChanges"], m => Router = m);
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import type { WebpackInstance } from "discord-types/other";
|
import type { WebpackInstance } from "discord-types/other";
|
||||||
|
|
||||||
|
export let _resolveReady: () => void;
|
||||||
|
/**
|
||||||
|
* Fired once a gateway connection to Discord has been established.
|
||||||
|
* This indicates that the core webpack modules have been initialised
|
||||||
|
*/
|
||||||
|
export const onceReady = new Promise<void>(r => _resolveReady = r);
|
||||||
|
|
||||||
export let wreq: WebpackInstance;
|
export let wreq: WebpackInstance;
|
||||||
export let cache: WebpackInstance["c"];
|
export let cache: WebpackInstance["c"];
|
||||||
|
|
||||||
|
@ -68,8 +75,19 @@ export function findAll(filter: FilterFn, getDefault = true) {
|
||||||
const ret = [] as any[];
|
const ret = [] as any[];
|
||||||
for (const key in cache) {
|
for (const key in cache) {
|
||||||
const mod = cache[key];
|
const mod = cache[key];
|
||||||
if (mod?.exports && filter(mod.exports)) ret.push(mod.exports);
|
if (!mod?.exports) continue;
|
||||||
if (mod?.exports?.default && filter(mod.exports.default)) ret.push(getDefault ? mod.exports.default : mod.exports);
|
|
||||||
|
if (filter(mod.exports))
|
||||||
|
ret.push(mod.exports);
|
||||||
|
else if (typeof mod.exports !== "object")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (mod.exports.default && filter(mod.exports.default))
|
||||||
|
ret.push(getDefault ? mod.exports.default : mod.exports);
|
||||||
|
else for (const nestedMod in mod.exports) if (nestedMod.length < 3) {
|
||||||
|
const nested = mod.exports[nestedMod];
|
||||||
|
if (nested && filter(nested)) ret.push(nested);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"target": "ESNEXT",
|
"target": "ESNEXT",
|
||||||
// https://esbuild.github.io/api/#jsx-factory
|
// https://esbuild.github.io/api/#jsx-factory
|
||||||
"jsxFactory": "Vencord.Webpack.Common.React.createElement",
|
"jsxFactory": "Vencord.Webpack.Common.React.createElement",
|
||||||
|
"jsxFragmentFactory": "Vencord.Webpack.Common.React.Fragment",
|
||||||
"jsx": "react"
|
"jsx": "react"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*"]
|
||||||
|
|
Loading…
Reference in a new issue