Vencord/src/components/VencordSettings/Updater.tsx

257 lines
9.6 KiB
TypeScript

/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* 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/>.
*/
import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { onlyOnce } from "@utils/onlyOnce";
import { useAwaiter } from "@utils/react";
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
import gitHash from "~git-hash";
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: (
<ErrorCard>
{err.split("\n").map(line => <div>{Parser.parse(line)}</div>)}
</ErrorCard>
)
});
}
finally {
dispatcher(false);
}
};
}
interface CommonProps {
repo: string;
repoPending: boolean;
}
function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {
return <Link href={`${repo}/commit/${hash}`} disabled={disabled}>
{hash}
</Link>;
}
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
return (
<Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => (
<div>
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
<span style={{
marginLeft: "0.5em",
color: "var(--text-normal)"
}}>{message} - {author}</span>
</div>
))}
</Card>
);
}
function Updatable(props: CommonProps) {
const [updates, setUpdates] = React.useState(changes);
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const isOutdated = (updates?.length ?? 0) > 0;
return (
<>
{!updates && updateError ? (
<>
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
<ErrorCard style={{ padding: "1em" }}>
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
</ErrorCard>
</>
) : (
<Forms.FormText className={Margins.bottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText>
)}
{isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.bottom8, Margins.top8)}>
{isOutdated && <Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
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() {
relaunch();
r();
},
onCancel: r
});
});
}
})}
>
Update Now
</Button>}
<Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsChecking, async () => {
const outdated = await checkForUpdates();
if (outdated) {
setUpdates(changes);
} else {
setUpdates([]);
Toasts.show({
message: "No updates found!",
id: Toasts.genId(),
type: Toasts.Type.MESSAGE,
options: {
position: Toasts.Position.BOTTOM
}
});
}
})}
>
Check for Updates
</Button>
</Flex>
</>
);
}
function Newer(props: CommonProps) {
return (
<>
<Forms.FormText className={Margins.bottom8}>
Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText>
<Changes {...props} updates={changes} />
</>
);
}
function Updater() {
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => {
if (err)
UpdateLogger.error("Failed to retrieve repo", err);
}, [err]);
const commonProps: CommonProps = {
repo,
repoPending
};
return (
<Forms.FormSection className={Margins.top16}>
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a notification on startup"
disabled={settings.autoUpdate}
>
Get notified about new updates
</Switch>
<Switch
value={settings.autoUpdate}
onChange={(v: boolean) => settings.autoUpdate = v}
note="Automatically update Vencord without confirmation prompt"
>
Automatically update
</Switch>
<Switch
value={settings.autoUpdateNotification}
onChange={(v: boolean) => settings.autoUpdateNotification = v}
note="Shows a notification when Vencord automatically updates"
disabled={!settings.autoUpdate}
>
Get notified when an automatic update completes
</Switch>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText className="vc-text-selectable">
{repoPending
? repo
: err
? "Failed to retrieve - check console"
: (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)
}
{" "}(<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)
</Forms.FormText>
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
</Forms.FormSection >
);
}
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
onError: onlyOnce(handleComponentFailed),
});