diff --git a/src/Vencord.ts b/src/Vencord.ts
index 73b53e84..a23b1a8b 100644
--- a/src/Vencord.ts
+++ b/src/Vencord.ts
@@ -30,17 +30,44 @@ import "./webpack/patchWebpack";
import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
-import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater";
+import { localStorage } from "./utils/localStorage";
+import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
+import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
export let Components: any;
+async function syncSettings() {
+ if (
+ Settings.cloud.settingsSync && // if it's enabled
+ Settings.cloud.authenticated // if cloud integrations are enabled
+ ) {
+ if (localStorage.Vencord_settingsDirty) {
+ await putCloudSettings();
+ delete localStorage.Vencord_settingsDirty;
+ } else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
+ // we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
+ // potential notifications that might occur. getCloudSettings() will always send a notification regardless if
+ // there was an error to notify the user, but besides that we only want to show one notification instead of all
+ // of the possible ones it has (such as when your settings are newer).
+ showNotification({
+ title: "Cloud Settings",
+ body: "Your settings have been updated! Click here to restart to fully apply changes!",
+ color: "var(--green-360)",
+ onClick: () => window.DiscordNative.app.relaunch()
+ });
+ }
+ }
+}
+
async function init() {
await onceReady;
startAllPlugins();
Components = await import("./components");
+ syncSettings();
+
if (!IS_WEB) {
try {
const isOutdated = await checkForUpdates();
diff --git a/src/api/settings.ts b/src/api/settings.ts
index 321a4c42..8a7d9ffd 100644
--- a/src/api/settings.ts
+++ b/src/api/settings.ts
@@ -16,9 +16,12 @@
* along with this program. If not, see .
*/
+import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
+import { localStorage } from "@utils/localStorage";
import Logger from "@utils/Logger";
import { mergeDefaults } from "@utils/misc";
+import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common";
@@ -49,6 +52,13 @@ export interface Settings {
useNative: "always" | "never" | "not-focused";
logLimit: number;
};
+
+ cloud: {
+ authenticated: boolean;
+ url: string;
+ settingsSync: boolean;
+ settingsSyncVersion: number;
+ };
}
const DefaultSettings: Settings = {
@@ -69,6 +79,13 @@ const DefaultSettings: Settings = {
position: "bottom-right",
useNative: "not-focused",
logLimit: 50
+ },
+
+ cloud: {
+ authenticated: false,
+ url: "https://api.vencord.dev/",
+ settingsSync: false,
+ settingsSyncVersion: 0
}
};
@@ -80,6 +97,13 @@ try {
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
}
+const saveSettingsOnFrequentAction = debounce(async () => {
+ if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
+ await putCloudSettings();
+ delete localStorage.Vencord_settingsDirty;
+ }
+}, 60_000);
+
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
const subscriptions = new Set();
@@ -142,6 +166,9 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
}
}
// And don't forget to persist the settings!
+ PlainSettings.cloud.settingsSyncVersion = Date.now();
+ localStorage.Vencord_settingsDirty = true;
+ saveSettingsOnFrequentAction();
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
return true;
}
diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx
new file mode 100644
index 00000000..3452cef5
--- /dev/null
+++ b/src/components/VencordSettings/CloudTab.tsx
@@ -0,0 +1,164 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 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 .
+*/
+
+import { showNotification } from "@api/Notifications";
+import { Settings, useSettings } from "@api/settings";
+import { CheckedTextInput } from "@components/CheckedTextInput";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Link } from "@components/Link";
+import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
+import { Margins } from "@utils/margins";
+import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
+import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
+
+function validateUrl(url: string) {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return "Invalid URL";
+ }
+}
+
+async function eraseAllData() {
+ const res = await fetch(new URL("/v1/", getCloudUrl()), {
+ method: "DELETE",
+ headers: new Headers({
+ Authorization: await getCloudAuth()
+ })
+ });
+
+ if (!res.ok) {
+ cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
+ showNotification({
+ title: "Cloud Integrations",
+ body: `Could not erase all data (API returned ${res.status}), please contact support.`,
+ color: "var(--red-360)"
+ });
+ return;
+ }
+
+ Settings.cloud.authenticated = false;
+ await deauthorizeCloud();
+
+ showNotification({
+ title: "Cloud Integrations",
+ body: "Successfully erased all data.",
+ color: "var(--green-360)"
+ });
+}
+
+function SettingsSyncSection() {
+ const { cloud } = useSettings(["cloud.authenticated", "cloud.settingsSync"]);
+ const sectionEnabled = cloud.authenticated && cloud.settingsSync;
+
+ return (
+
+
+ Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
+ minimal effort.
+
+ { cloud.settingsSync = v; }}
+ >
+ Settings Sync
+
+