Equicord/src/api/SettingsSync/offline.ts

162 lines
5 KiB
TypeScript
Raw Normal View History

/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { PlainSettings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { chooseFile, saveFile } from "@utils/web";
import { moment, Toasts } from "@webpack/common";
2025-11-18 23:48:50 -05:00
import { DataStore } from "..";
const toast = (type: string, message: string) =>
Toasts.show({
type,
message,
id: Toasts.genId()
});
const toastSuccess = () =>
toast(Toasts.Type.SUCCESS, "Settings successfully imported. Restart to apply changes!");
const toastFailure = (err: any) =>
toast(Toasts.Type.FAILURE, `Failed to import settings: ${String(err)}`);
const logger = new Logger("SettingsSync:Offline", "#39b7e0");
function isSafeObject(obj: any) {
if (obj == null || typeof obj !== "object") return true;
for (const key in obj) {
if (["__proto__", "constructor", "prototype"].includes(key)) {
return false;
}
if (!isSafeObject(obj[key])) {
return false;
}
}
return true;
}
export async function importSettings(data: string) {
try {
var parsed = JSON.parse(data);
} catch (err) {
console.log(data);
throw new Error("Failed to parse JSON: " + String(err));
}
if (!isSafeObject(parsed))
throw new Error("Unsafe Settings");
if ("settings" in parsed && "quickCss" in parsed) {
Object.assign(PlainSettings, parsed.settings);
await VencordNative.settings.set(parsed.settings);
await VencordNative.quickCss.set(parsed.quickCss);
2025-12-04 12:07:19 -05:00
if (parsed.dataStore) await DataStore.setMany(parsed.dataStore);
} else
throw new Error("Invalid Settings. Is this even a Vencord Settings file?");
}
2025-12-04 12:01:17 -05:00
export async function exportSettings({ syncDataStore = true, minify }: { syncDataStore?: boolean; minify?: boolean; }) {
const settings = VencordNative.settings.get();
const quickCss = await VencordNative.quickCss.get();
2025-12-04 12:01:17 -05:00
const dataStore = syncDataStore ? await DataStore.entries() : undefined;
2025-12-04 11:59:27 -05:00
return JSON.stringify(
{
settings,
quickCss,
2025-12-04 12:01:17 -05:00
...(syncDataStore && { dataStore })
2025-12-04 11:59:27 -05:00
},
null,
minify ? undefined : 4
);
2025-11-18 23:48:50 -05:00
}
export async function exportPlugins({ minify }: { minify?: boolean; } = {}) {
const { plugins } = VencordNative.settings.get();
return JSON.stringify({ settings: { plugins } }, null, minify ? undefined : 4);
}
export async function exportCSS({ minify }: { minify?: boolean; } = {}) {
const quickCss = await VencordNative.quickCss.get();
return JSON.stringify({ quickCss }, null, minify ? undefined : 4);
}
2025-11-18 23:48:50 -05:00
export async function exportDataStores({ minify }: { minify?: boolean; } = {}) {
const dataStore = await DataStore.entries();
return JSON.stringify({ dataStore }, null, minify ? undefined : 4);
}
type BackupType = "settings" | "plugins" | "css" | "datastore";
export async function downloadSettingsBackup(type: BackupType, { minify }: { minify?: boolean; } = {}) {
let backup: string;
switch (type) {
case "settings":
backup = await exportSettings({ minify });
break;
case "plugins":
backup = await exportPlugins({ minify });
break;
case "css":
backup = await exportCSS({ minify });
break;
case "datastore":
backup = await exportDataStores({ minify });
break;
default:
throw new Error("Invalid backup type");
}
const filename = `equicord-${type}-backup-${moment().format("YYYY-MM-DD")}.json`;
const data = new TextEncoder().encode(backup);
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(data, filename);
} else {
saveFile(new File([data], filename, { type: "application/json" }));
}
}
export async function uploadSettingsBackup(showToast = true): Promise<void> {
if (IS_DISCORD_DESKTOP) {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "Vencord Settings Backup", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
await importSettings(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {
logger.error(err);
if (showToast) toastFailure(err);
}
}
} else {
const file = await chooseFile("application/json");
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
try {
await importSettings(reader.result as string);
if (showToast) toastSuccess();
} catch (err) {
logger.error(err);
if (showToast) toastFailure(err);
}
};
reader.readAsText(file);
}
}