feat: new folder structure

This commit is contained in:
smartfrigde 2024-05-15 18:14:49 +02:00
parent 8aca371346
commit 419cb8eb4a
48 changed files with 536 additions and 1059 deletions

157
src/common/config.ts Normal file
View file

@ -0,0 +1,157 @@
import {app, dialog} from "electron";
import path from "path";
import fs from "fs";
import {getWindowStateLocation} from "./windowState";
export let firstRun: boolean;
export function checkForDataFolder(): void {
const dataPath = path.join(path.dirname(app.getPath("exe")), "armcord-data");
if (fs.existsSync(dataPath) && fs.statSync(dataPath).isDirectory()) {
console.log("Found armcord-data folder. Running in portable mode.");
app.setPath("userData", dataPath);
}
}
export interface Settings {
// Referenced for detecting a broken config.
"0"?: string;
// Referenced once for disabling mod updating.
noBundleUpdates?: boolean;
// Only used for external url warning dialog.
ignoreProtocolWarning?: boolean;
customIcon: string;
windowStyle: string;
channel: string;
armcordCSP: boolean;
minimizeToTray: boolean;
multiInstance: boolean;
spellcheck: boolean;
mods: string;
dynamicIcon: boolean;
mobileMode: boolean;
skipSplash: boolean;
performanceMode: string;
customJsBundle: RequestInfo | URL;
customCssBundle: RequestInfo | URL;
startMinimized: boolean;
useLegacyCapturer: boolean;
tray: boolean;
keybinds: Array<string>;
inviteWebsocket: boolean;
disableAutogain: boolean;
trayIcon: string;
doneSetup: boolean;
clientName: string;
}
export function getConfigLocation(): string {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}settings.json`;
}
export async function getConfig<K extends keyof Settings>(object: K): Promise<Settings[K]> {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let returndata = JSON.parse(rawdata);
return returndata[object];
}
export function getConfigSync<K extends keyof Settings>(object: K) {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let returndata = JSON.parse(rawdata);
return returndata[object];
}
export async function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): Promise<void> {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let parsed = JSON.parse(rawdata);
parsed[object] = toSet;
let toSave = JSON.stringify(parsed, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
}
export async function setConfigBulk(object: Settings): Promise<void> {
let existingData = {};
try {
const existingDataBuffer = fs.readFileSync(getConfigLocation(), "utf-8");
existingData = JSON.parse(existingDataBuffer.toString());
} catch (error) {
// Ignore errors when the file doesn't exist or parsing fails
}
// Merge the existing data with the new data
const mergedData = {...existingData, ...object};
// Write the merged data back to the file
const toSave = JSON.stringify(mergedData, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
}
export async function checkIfConfigExists(): Promise<void> {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}settings.json`;
if (!fs.existsSync(settingsFile)) {
if (!fs.existsSync(storagePath)) {
fs.mkdirSync(storagePath);
console.log("Created missing storage folder");
}
console.log("First run of the ArmCord. Starting setup.");
setup();
firstRun = true;
} else if ((await getConfig("doneSetup")) == false) {
console.log("First run of the ArmCord. Starting setup.");
setup();
firstRun = true;
} else {
console.log("ArmCord has been run before. Skipping setup.");
}
}
export async function checkIfConfigIsBroken(): Promise<void> {
try {
let settingsData = fs.readFileSync(getConfigLocation(), "utf-8");
JSON.parse(settingsData);
console.log("Config is fine");
} catch (e) {
console.error(e);
console.log("Detected a corrupted config");
setup();
dialog.showErrorBox(
"Oops, something went wrong.",
"ArmCord has detected that your configuration file is corrupted, please restart the app and set your settings again. If this issue persists, report it on the support server/Github issues."
);
}
try {
let windowData = fs.readFileSync(getWindowStateLocation(), "utf-8");
JSON.parse(windowData);
console.log("Window config is fine");
} catch (e) {
console.error(e);
fs.writeFileSync(getWindowStateLocation(), "{}", "utf-8");
console.log("Detected a corrupted window config");
}
}
export function setup(): void {
console.log("Setting up temporary ArmCord settings.");
const defaults: Settings = {
windowStyle: "default",
channel: "stable",
armcordCSP: true,
minimizeToTray: true,
keybinds: [],
multiInstance: false,
mods: "none",
spellcheck: true,
performanceMode: "none",
skipSplash: false,
inviteWebsocket: true,
startMinimized: false,
dynamicIcon: false,
tray: true,
customJsBundle: "https://armcord.app/placeholder.js",
customCssBundle: "https://armcord.app/placeholder.css",
disableAutogain: false,
useLegacyCapturer: false,
mobileMode: false,
trayIcon: "default",
doneSetup: false,
clientName: "ArmCord",
customIcon: path.join(__dirname, "../", "/assets/desktop.png")
};
setConfigBulk({
...defaults
});
}

20
src/common/dom.ts Normal file
View file

@ -0,0 +1,20 @@
export function addStyle(styleString: string): void {
const style = document.createElement("style");
style.textContent = styleString;
document.head.append(style);
}
export function addScript(scriptString: string): void {
let script = document.createElement("script");
script.textContent = scriptString;
document.body.append(script);
}
export async function injectJS(inject: string): Promise<void> {
const js = await (await fetch(`${inject}`)).text();
const el = document.createElement("script");
el.appendChild(document.createTextNode(js));
document.body.appendChild(el);
}

47
src/common/flags.ts Normal file
View file

@ -0,0 +1,47 @@
import {app} from "electron";
import {getConfig} from "./config";
export let transparency: boolean;
export async function injectElectronFlags(): Promise<void> {
// MIT License
// Copyright (c) 2022 GooseNest
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
const presets = {
performance: `--enable-gpu-rasterization --enable-zero-copy --ignore-gpu-blocklist --enable-hardware-overlays=single-fullscreen,single-on-top,underlay --enable-features=EnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilation --disable-features=Vulkan --force_high_performance_gpu`, // Performance
battery: "--enable-features=TurnOffStreamingMediaCachingOnBattery --force_low_power_gpu", // Known to have better battery life for Chromium?
vaapi: "--ignore-gpu-blocklist --enable-features=VaapiVideoDecoder --enable-gpu-rasterization --enable-zero-copy --force_high_performance_gpu --use-gl=desktop --disable-features=UseChromeOSDirectVideoDecoder"
};
switch (await getConfig("performanceMode")) {
case "performance":
console.log("Performance mode enabled");
app.commandLine.appendArgument(presets.performance);
break;
case "battery":
console.log("Battery mode enabled");
app.commandLine.appendArgument(presets.battery);
break;
default:
console.log("No performance modes set");
}
if ((await getConfig("windowStyle")) == "transparent" && process.platform === "win32") {
transparency = true;
}
}

67
src/common/lang.ts Normal file
View file

@ -0,0 +1,67 @@
import {app} from "electron";
import path from "path";
import fs from "fs";
export async function setLang(language: string): Promise<void> {
const langConfigFile = `${path.join(app.getPath("userData"), "/storage/")}lang.json`;
if (!fs.existsSync(langConfigFile)) {
fs.writeFileSync(langConfigFile, "{}", "utf-8");
}
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
parsed.lang = language;
let toSave = JSON.stringify(parsed, null, 4);
fs.writeFileSync(langConfigFile, toSave, "utf-8");
}
let language: string;
export async function getLang(object: string): Promise<string> {
if (language == undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
language = parsed.lang;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
language = "en-US";
}
}
if (language.length == 2) {
language = `${language}-${language.toUpperCase()}`;
}
let langPath = path.join(__dirname, "../", `/assets/lang/${language}.json`);
if (!fs.existsSync(langPath)) {
langPath = path.join(__dirname, "../", "/assets/lang/en-US.json");
}
let rawdata = fs.readFileSync(langPath, "utf-8");
let parsed = JSON.parse(rawdata);
if (parsed[object] == undefined) {
console.log(`${object} is undefined in ${language}`);
langPath = path.join(__dirname, "../", "/assets/lang/en-US.json");
rawdata = fs.readFileSync(langPath, "utf-8");
parsed = JSON.parse(rawdata);
return parsed[object];
} else {
return parsed[object];
}
}
export async function getLangName(): Promise<string> {
if (language == undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
language = parsed.lang;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
language = "en-US";
}
}
if (language.length == 2) {
language = `${language}-${language.toUpperCase()}`;
}
return language;
}

3
src/common/sleep.ts Normal file
View file

@ -0,0 +1,3 @@
export async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

19
src/common/version.ts Normal file
View file

@ -0,0 +1,19 @@
import {app} from "electron";
export const packageVersion = require("../package.json").version;
export function getVersion(): string {
return packageVersion;
}
export function getDisplayVersion(): string {
//Checks if the app version # has 4 sections (3.1.0.0) instead of 3 (3.1.0) / Shitty way to check if Kernel Mod is installed
if ((app.getVersion() == packageVersion) == false) {
if ((app.getVersion() == process.versions.electron) == true) {
return `Dev Build (${packageVersion})`;
} else {
return `${packageVersion} [Modified]`;
}
} else {
return packageVersion;
}
}

34
src/common/windowState.ts Normal file
View file

@ -0,0 +1,34 @@
import {app} from "electron";
import path from "path";
import fs from "fs";
export interface WindowState {
width: number;
height: number;
x: number;
y: number;
isMaximized: boolean;
}
export function getWindowStateLocation() {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}window.json`;
}
export async function setWindowState(object: WindowState): Promise<void> {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const saveFile = `${storagePath}window.json`;
let toSave = JSON.stringify(object, null, 4);
fs.writeFileSync(saveFile, toSave, "utf-8");
}
export async function getWindowState<K extends keyof WindowState>(object: K): Promise<WindowState[K]> {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}window.json`;
if (!fs.existsSync(settingsFile)) {
fs.writeFileSync(settingsFile, "{}", "utf-8");
}
let rawdata = fs.readFileSync(settingsFile, "utf-8");
let returndata = JSON.parse(rawdata);
console.log(`[Window state manager] ${returndata}`);
return returndata[object];
}

View file

@ -1,5 +1,5 @@
import electron from "electron";
import {getConfig} from "../utils";
import {getConfig} from "../../common/config";
const unstrictCSP = (): void => {
console.log("Setting up CSP unstricter...");

View file

@ -0,0 +1,124 @@
import {app, dialog} from "electron";
import extract from "extract-zip";
import path from "path";
import {getConfig} from "../../common/config";
import fs from "fs";
import util from "util";
const streamPipeline = util.promisify(require("stream").pipeline);
async function updateModBundle(): Promise<void> {
if ((await getConfig("noBundleUpdates")) == undefined ?? false) {
try {
console.log("Downloading mod bundle");
const distFolder = `${app.getPath("userData")}/plugins/loader/dist/`;
while (!fs.existsSync(distFolder)) {
//waiting
}
let name: string = await getConfig("mods");
if (name == "custom") {
// aspy fix
let bundle: string = await (await fetch(await getConfig("customJsBundle"))).text();
fs.writeFileSync(`${distFolder}bundle.js`, bundle, "utf-8");
let css: string = await (await fetch(await getConfig("customCssBundle"))).text();
fs.writeFileSync(`${distFolder}bundle.css`, css, "utf-8");
} else {
const clientMods = {
vencord: "https://github.com/Vendicated/Vencord/releases/download/devbuild/browser.js",
shelter: "https://raw.githubusercontent.com/uwu/shelter-builds/main/shelter.js"
};
const clientModsCss = {
vencord: "https://github.com/Vendicated/Vencord/releases/download/devbuild/browser.css",
shelter: "https://armcord.app/placeholder.css"
};
console.log(clientMods[name as keyof typeof clientMods]);
let bundle: string = await (await fetch(clientMods[name as keyof typeof clientMods])).text();
fs.writeFileSync(`${distFolder}bundle.js`, bundle, "utf-8");
let css: string = await (await fetch(clientModsCss[name as keyof typeof clientModsCss])).text();
fs.writeFileSync(`${distFolder}bundle.css`, css, "utf-8");
}
} catch (e) {
console.log("[Mod loader] Failed to install mods");
console.error(e);
dialog.showErrorBox(
"Oops, something went wrong.",
"ArmCord couldn't install mods, please check if you have stable internet connection and restart the app. If this issue persists, report it on the support server/Github issues."
);
}
} else {
console.log("[Mod loader] Skipping mod bundle update");
}
}
export let modInstallState: string;
export function updateModInstallState() {
modInstallState = "modDownload";
updateModBundle();
import("./plugin");
modInstallState = "done";
}
export async function installModLoader(): Promise<void> {
if ((await getConfig("mods")) == "none") {
modInstallState = "none";
fs.rmSync(`${app.getPath("userData")}/plugins/loader`, {recursive: true, force: true});
import("./plugin");
console.log("[Mod loader] Skipping");
return;
}
const pluginFolder = `${app.getPath("userData")}/plugins/`;
if (fs.existsSync(`${pluginFolder}loader`) && fs.existsSync(`${pluginFolder}loader/dist/bundle.css`)) {
updateModInstallState();
return;
}
try {
fs.rmSync(`${app.getPath("userData")}/plugins/loader`, {recursive: true, force: true});
modInstallState = "installing";
let zipPath = `${app.getPath("temp")}/loader.zip`;
if (!fs.existsSync(pluginFolder)) {
fs.mkdirSync(pluginFolder);
console.log("[Mod loader] Created missing plugin folder");
}
// Add more of these later if needed!
let URLs = [
"https://armcord.app/loader.zip",
"https://armcord.vercel.app/loader.zip",
"https://raw.githubusercontent.com/ArmCord/website/new/public/loader.zip"
];
let loaderZip: any;
while (true) {
if (URLs.length <= 0) throw new Error(`unexpected response ${loaderZip.statusText}`);
try {
loaderZip = await fetch(URLs[0]);
} catch (err) {
console.log("[Mod loader] Failed to download. Links left to try: " + (URLs.length - 1));
URLs.splice(0, 1);
continue;
}
break;
}
await streamPipeline(loaderZip.body, fs.createWriteStream(zipPath));
await extract(zipPath, {dir: path.join(app.getPath("userData"), "plugins")});
updateModInstallState();
} catch (e) {
console.log("[Mod loader] Failed to install modloader");
console.error(e);
dialog.showErrorBox(
"Oops, something went wrong.",
"ArmCord couldn't install internal mod loader, please check if you have stable internet connection and restart the app. If this issue persists, report it on the support server/Github issues."
);
}
}

View file

@ -1,28 +1,20 @@
//ipc stuff
import {app, clipboard, desktopCapturer, ipcMain, nativeImage, shell} from "electron";
import {mainWindow} from "./window";
import {
Settings,
getConfig,
getConfigLocation,
getDisplayVersion,
getLang,
getLangName,
getVersion,
modInstallState,
packageVersion,
setConfigBulk,
setLang,
sleep
} from "./utils";
import {customTitlebar} from "./main";
import {createSettingsWindow} from "./settings/main";
import os from "os";
import fs from "fs";
import path from "path";
import {createTManagerWindow} from "./themeManager/main";
import {splashWindow} from "./splash/main";
import {createKeybindWindow} from "./keybindMaker/main";
import {getConfig, setConfigBulk, getConfigLocation, Settings} from "../common/config";
import {setLang, getLang, getLangName} from "../common/lang";
import {sleep} from "../common/sleep";
import {getVersion, getDisplayVersion, packageVersion} from "../common/version";
import {customTitlebar} from "../main";
import {createSettingsWindow} from "../settings/main";
import {splashWindow} from "../splash/main";
import {createTManagerWindow} from "../themeManager/main";
import {modInstallState} from "./extensions/mods";
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const themesPath = path.join(userDataPath, "/themes/");
@ -136,9 +128,6 @@ export function registerIpc(): void {
ipcMain.on("openManagerWindow", () => {
createTManagerWindow();
});
ipcMain.on("openKeybindWindow", () => {
createKeybindWindow();
});
ipcMain.on("setting-armcordCSP", async (event) => {
if (await getConfig("armcordCSP")) {
event.returnValue = true;

View file

@ -1,6 +1,6 @@
import {BrowserWindow, Menu, app, clipboard} from "electron";
import {mainWindow} from "./window";
import {createSettingsWindow} from "./settings/main";
import {createSettingsWindow} from "../settings/main";
function paste(contents: any): void {
const contentTypes = clipboard.availableFormats().toString();

View file

@ -1,8 +1,8 @@
//Fixed context isolation version https://github.com/getferdi/ferdi/blob/develop/src/webview/screenshare.ts
//original https://github.com/electron/electron/issues/16513#issuecomment-602070250
import {addScript, addStyle} from "../utils";
import fs from "fs";
import path from "path";
import {addScript, addStyle} from "../../common/dom";
const CANCEL_ID = "desktop-capturer-selection__cancel";

View file

@ -1,4 +1,4 @@
import {addStyle} from "../utils";
import {addStyle} from "../../common/dom";
import * as fs from "fs";
import * as path from "path";
export function injectMobileStuff(): void {

View file

@ -4,10 +4,11 @@ import "./settings";
import {ipcRenderer} from "electron";
import * as fs from "fs";
import * as path from "path";
import {addScript, addStyle, sleep} from "../utils";
import {injectMobileStuff} from "./mobile";
import {fixTitlebar, injectTitlebar} from "./titlebar";
import {injectSettings} from "./settings";
import {addStyle, addScript} from "../../common/dom";
import {sleep} from "../../common/sleep";
window.localStorage.setItem("hideNag", "true");
@ -56,10 +57,10 @@ sleep(5000).then(async () => {
})();
`);
if (ipcRenderer.sendSync("disableAutogain")) {
addScript(fs.readFileSync(path.join(__dirname, "../", "/content/js/disableAutogain.js"), "utf8"));
addScript(fs.readFileSync(path.join(__dirname, "../", "../", "/content/js/disableAutogain.js"), "utf8"));
}
addScript(fs.readFileSync(path.join(__dirname, "../", "/content/js/rpc.js"), "utf8"));
const cssPath = path.join(__dirname, "../", "/content/css/discord.css");
addScript(fs.readFileSync(path.join(__dirname, "../", "../", "/content/js/rpc.js"), "utf8"));
const cssPath = path.join(__dirname, "../", "../", "/content/css/discord.css");
addStyle(fs.readFileSync(cssPath, "utf8"));
await updateLang();
});
@ -107,21 +108,16 @@ setInterval(() => {
const acSettings = advanced.cloneNode(true) as HTMLElement;
const tManager = advanced.cloneNode(true) as HTMLElement;
const fQuit = advanced.cloneNode(true) as HTMLElement;
const keybindMaker = advanced.cloneNode(true) as HTMLElement;
acSettings.textContent = "ArmCord Settings";
acSettings.id = "acSettings";
acSettings.onclick = () => injectSettings();
tManager.textContent = "Themes";
tManager.id = "acThemes";
tManager.onclick = () => ipcRenderer.send("openManagerWindow");
keybindMaker.textContent = "Global keybinds";
keybindMaker.id = "acKeybinds";
keybindMaker.onclick = () => ipcRenderer.send("openKeybindWindow");
fQuit.textContent = "Force Quit";
fQuit.id = "acForceQuit";
fQuit.onclick = () => ipcRenderer.send("win-quit");
advanced.insertAdjacentElement("afterend", acSettings);
advanced.insertAdjacentElement("afterend", tManager);
advanced.insertAdjacentElement("afterend", keybindMaker);
advanced.insertAdjacentElement("afterend", fQuit);
}, 1000);

View file

@ -1,14 +1,15 @@
import * as path from "path";
import * as fs from "fs";
import {addStyle} from "../utils";
import {addStyle} from "../../common/dom";
import {WebviewTag} from "electron";
var webview = `<webview src="${path.join("file://", __dirname, "../", "/settings/settings.html")}" preload="${path.join(
var webview = `<webview src="${path.join(
"file://",
__dirname,
"../",
"/settings/preload.js"
)}" id="inAppSettings"></webview>`;
"../",
"/settings/settings.html"
)}" preload="${path.join("file://", __dirname, "../", "../", "/settings/preload.js")}" id="inAppSettings"></webview>`;
export function injectSettings() {
document.getElementById("webviewSettingsContainer")!.innerHTML = webview;

View file

@ -1,5 +1,5 @@
import {ipcRenderer} from "electron";
import {addStyle} from "../utils";
import {addStyle} from "../../common/dom";
import * as fs from "fs";
import * as path from "path";
import os from "os";
@ -21,8 +21,8 @@ export function injectTitlebar(): void {
} else {
document.getElementById("app-mount")!.prepend(elem);
}
const titlebarcssPath = path.join(__dirname, "../", "/content/css/titlebar.css");
const wordmarkcssPath = path.join(__dirname, "../", "/content/css/logos.css");
const titlebarcssPath = path.join(__dirname, "../", "../", "/content/css/titlebar.css");
const wordmarkcssPath = path.join(__dirname, "../", "../", "/content/css/logos.css");
addStyle(fs.readFileSync(titlebarcssPath, "utf8"));
addStyle(fs.readFileSync(wordmarkcssPath, "utf8"));
document.body.setAttribute("customTitlebar", "");

View file

@ -1,6 +1,6 @@
import {BrowserWindow, MessageBoxOptions, desktopCapturer, dialog, ipcMain, session} from "electron";
import path from "path";
import {iconPath} from "../main";
import {iconPath} from "../../main";
let capturerWindow: BrowserWindow;
function showAudioDialog(): boolean {
const options: MessageBoxOptions = {

View file

@ -4,22 +4,15 @@
// I'm sorry for this mess but I'm not sure how to fix it.
import {BrowserWindow, MessageBoxOptions, app, dialog, nativeImage, shell} from "electron";
import path from "path";
import {
contentPath,
firstRun,
getConfig,
getWindowState,
registerGlobalKeybinds,
setConfig,
setWindowState
} from "./utils";
import {registerIpc} from "./ipc";
import {setMenu} from "./menu";
import * as fs from "fs";
import contextMenu from "electron-context-menu";
import os from "os";
import {tray} from "./tray";
import {iconPath} from "./main";
import {tray} from "../tray";
import {iconPath} from "../main";
import {getConfig, setConfig, firstRun} from "../common/config";
import {getWindowState, setWindowState} from "../common/windowState";
export let mainWindow: BrowserWindow;
export let inviteWindow: BrowserWindow;
let forceQuit = false;
@ -196,7 +189,6 @@ async function doAfterDefiningTheWindow(): Promise<void> {
if (!fs.existsSync(`${userDataPath}/disabled.txt`)) {
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), "");
}
registerGlobalKeybinds();
mainWindow.webContents.on("did-finish-load", () => {
fs.readdirSync(themesFolder).forEach((file) => {
try {
@ -265,7 +257,6 @@ async function doAfterDefiningTheWindow(): Promise<void> {
mainWindow.on("unmaximize", () => {
mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`);
});
console.log(contentPath);
if ((await getConfig("inviteWebsocket")) == true) {
require("arrpc");
//await startServer();
@ -274,7 +265,7 @@ async function doAfterDefiningTheWindow(): Promise<void> {
mainWindow.close();
}
//loadURL broke for no good reason after E28
mainWindow.loadFile(`${__dirname}/splash/redirect.html`);
mainWindow.loadFile(`${__dirname}/../splash/redirect.html`);
if (await getConfig("skipSplash")) {
mainWindow.show();

View file

@ -1,66 +0,0 @@
import {BrowserWindow, globalShortcut, ipcMain, shell} from "electron";
import path from "path";
import {getConfig, registerGlobalKeybinds, setConfig} from "../utils";
let keybindWindow: BrowserWindow;
let instance = 0;
export function createKeybindWindow(): void {
console.log("Creating keybind maker window.");
instance += 1;
if (instance > 1) {
if (keybindWindow) {
keybindWindow.show();
keybindWindow.restore();
}
} else {
keybindWindow = new BrowserWindow({
width: 720,
height: 670,
title: `ArmCord Global Keybinds Maker`,
darkTheme: true,
frame: true,
backgroundColor: "#2f3136",
autoHideMenuBar: true,
webPreferences: {
sandbox: false,
preload: path.join(__dirname, "preload.js")
}
});
async function makerLoadPage(): Promise<void> {
globalShortcut.unregisterAll();
keybindWindow.loadURL(`file://${__dirname}/maker.html`);
}
keybindWindow.webContents.setWindowOpenHandler(({url}) => {
shell.openExternal(url);
return {action: "deny"};
});
ipcMain.on("addKeybind", async (_event, keybind) => {
var keybinds = await getConfig("keybinds");
keybind.replace(" ", "Space");
if (keybinds.includes(keybind)) return;
keybinds.push(keybind);
await setConfig("keybinds", keybinds);
keybindWindow.webContents.reload();
});
ipcMain.on("removeKeybind", async (_event, keybind) => {
var keybinds = await getConfig("keybinds");
const index = keybinds.indexOf(keybind);
keybinds.splice(index, 1);
await setConfig("keybinds", keybinds);
keybindWindow.webContents.reload();
});
keybindWindow.webContents.on("did-finish-load", async () => {
for (const keybind of await getConfig("keybinds")) {
console.log(keybind);
keybindWindow.webContents.send("keybindCombo", keybind);
}
});
keybindWindow.on("close", () => {
registerGlobalKeybinds();
});
makerLoadPage();
keybindWindow.on("close", () => {
instance = 0;
});
}
}

View file

@ -1,392 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
:root {
--background-secondary: #2f3136;
--background-secondary-alt: #292b2f;
--background-floating: #18191c;
--background-modifier-hover: rgba(106, 116, 128, 0.16);
--brand-experiment: #7289da;
--brand-experiment-560: #5c6fb1;
--brand-experiment-600: #4e5d94;
--interactive-normal: #b9bbbe;
--interactive-hover: #dcddde;
--text-muted: #72767d;
--font-primary: "Whitney";
--header-primary: #fff;
}
html {
font-size: 22px;
}
body {
padding: 1rem;
background: var(--background-secondary);
}
@font-face {
font-family: Whitney;
font-weight: 200;
font-style: normal;
src: url(https://armcord.app/whitney_400.woff) format("woff");
}
* {
font-family: "Whitney", sans-serif;
box-sizing: border-box;
margin: 0;
padding: 0;
cursor: default;
}
.card {
background-color: var(--background-floating);
color: white;
padding: 1rem;
border-color: var(--background-floating);
border-style: solid;
width: 100%;
border-radius: 10px;
}
.cards {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-gap: 1rem;
justify-content: space-evenly;
}
/* Screen larger than 600px? 2 column */
@media (min-width: 600px) {
.cards {
grid-template-columns: repeat(2, 1fr);
}
}
/* Screen larger than 900px? 3 columns */
@media (min-width: 900px) {
.cards {
grid-template-columns: repeat(3, 1fr);
}
}
.flex-box {
display: flex;
justify-content: space-between;
}
/* switches */
.tgl {
display: none;
}
.tgl::-moz-selection,
.tgl:after::-moz-selection,
.tgl:before::-moz-selection,
.tgl *::-moz-selection,
.tgl *:after::-moz-selection,
.tgl *:before::-moz-selection,
.tgl + .tgl-btn::-moz-selection {
background: none;
}
.tgl::selection,
.tgl:after::selection,
.tgl:before::selection,
.tgl *::selection,
.tgl *:after::selection,
.tgl *:before::selection,
.tgl + .tgl-btn::selection {
background: none;
}
.tgl + .tgl-btn {
outline: 0;
display: block;
width: 3em;
position: relative;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tgl + .tgl-btn:after,
.tgl + .tgl-btn:before {
position: relative;
display: block;
content: "";
width: 50%;
height: 100%;
}
.tgl + .tgl-btn:after {
left: 1px;
}
.tgl + .tgl-btn:before {
display: none;
}
.tgl:checked + .tgl-btn:after {
left: 56%;
}
.tgl-light + .tgl-btn {
background: var(--text-muted);
border-radius: 25px;
padding: 4px;
transition: all 0.4s ease;
}
.tgl-light + .tgl-btn:after {
border-radius: 50px;
position: relative;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
background: rgb(255, 255, 255);
transition: all 0.2s ease;
}
.tgl-light:checked + .tgl-btn {
background: var(--brand-experiment);
}
input[type="text"],
select {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #72767d;
background-color: #46484d;
color: white;
border-radius: 4px;
box-sizing: border-box;
}
#download {
margin-top: 3px;
height: 50px;
vertical-align: middle;
text-align: right;
z-index: 99;
}
/* The Modal (background) */
.modal {
display: none;
/* Hidden by default */
position: fixed;
/* Stay in place */
z-index: 1;
/* Sit on top */
padding-top: 100px;
/* Location of the box */
background-color: var(--background-secondary);
left: 0;
top: 0;
width: 100%;
/* Full width */
height: 100%;
/* Full height */
overflow: auto;
/* Enable scroll if needed */
background-color: rgb(0, 0, 0);
/* Fallback color */
background-color: rgba(0, 0, 0, 0.4);
/* Black w/ opacity */
}
/* Modal Content */
.modal-content {
position: relative;
margin: auto;
padding: 1rem;
background-color: var(--background-secondary);
border-color: var(--background-floating);
border-style: solid;
border-radius: 10px;
width: 80%;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
animation-duration: 0.4s;
}
/* Add Animation */
@-webkit-keyframes animatetop {
from {
top: -300px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
@keyframes animatetop {
from {
top: -300px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
/* The Close Button */
.close {
color: white;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: red;
text-decoration: none;
cursor: pointer;
}
.modal-header {
padding: 2px 16px;
color: white;
}
.modal-body {
padding: 2px 16px;
color: white;
}
.modal-footer {
padding: 2px 16px;
color: white;
}
a.button {
color: var(--brand-experiment-560);
}
.button {
margin-right: 10px;
}
img.themeInfoIcon {
height: 30px;
position: relative;
top: 5px;
}
.addKbd {
width: 100%;
height: 30px;
color: var(--header-primary);
border-radius: 8px;
border: none;
font-weight: bold;
background-color: var(--brand-experiment);
}
.addKbd:hover {
background-color: var(--brand-experiment-560);
color: var(--interactive-hover);
}
kbd {
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0.1em 0.5em;
position: relative;
margin: 0 0.2em;
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px var(--background-floating) inset;
background-color: #41434a;
color: var(--header-primary);
}
#output {
margin-top: 10px;
width: 100%;
margin-bottom: 10px;
min-height: 50px;
background-color: var(--background-secondary-alt);
border-radius: 8px;
border: 3px dashed var(--interactive-normal);
}
.preview {
top: 5px;
}
#warning {
color: yellow;
text-align: center;
font-size: 20px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<p id="warning">Global keybinds are disabled while this window is open.</p>
<button class="addKbd" id="startButton">Add a global keybind</button>
<div id="output"></div>
<div class="cards" id="cardBox"></div>
<script>
let captureStarted = false;
const outputElement = document.getElementById("output");
function startCapture() {
if (captureStarted) return;
let keys = "";
captureStarted = true;
outputElement.innerHTML = "";
const kbdElement = document.createElement("div");
outputElement.appendChild(kbdElement);
let capturedKeystrokes = "";
const startTime = Date.now();
function displayKeystrokes() {
const currentTime = Date.now();
const elapsedSeconds = Math.floor((currentTime - startTime) / 1000);
if (elapsedSeconds >= 5) {
captureStarted = false;
//remove last character from keys
manager.add(keys.slice(0, -1));
outputElement.innerHTML = "";
keys = "";
return;
}
kbdElement.innerHTML = capturedKeystrokes;
requestAnimationFrame(displayKeystrokes);
}
function handleKeystroke(event) {
const key = event.key;
if (keys.includes(key)) {
console.log("already in array");
} else {
keys += key + "+"; //electron friendly format
capturedKeystrokes += `<kbd class="preview">${key}</kbd>`;
}
}
document.addEventListener("keydown", handleKeystroke);
displayKeystrokes();
}
const startButton = document.getElementById("startButton");
startButton.addEventListener("click", startCapture);
</script>
</body>
</html>

View file

@ -1,36 +0,0 @@
import {contextBridge, ipcRenderer} from "electron";
import {sleep} from "../utils";
contextBridge.exposeInMainWorld("manager", {
add: (keybindName: string) => ipcRenderer.send("addKeybind", keybindName),
remove: (keybindName: string) => ipcRenderer.send("removeKeybind", keybindName)
});
ipcRenderer.on("keybindCombo", (_event, keybindName) => {
sleep(1000);
console.log(keybindName);
let e = document.getElementById("cardBox");
var keys = keybindName.split("+");
var id = keybindName.replace("+", "");
var html = "";
for (var key in keys) {
html += `<kbd>${keys[key]}</kbd>`;
}
e?.insertAdjacentHTML(
"beforeend",
`
<div class="card">
<div class="flex-box">
${html}
<input id="${id}" class="tgl tgl-light left" type="checkbox" />
<label class="tgl-btn left" for="${id}"></label>
</div>
</div>
`
);
(document.getElementById(id) as HTMLInputElement)!.checked = true;
(document.getElementById(id) as HTMLInputElement)!.addEventListener("input", function (evt) {
ipcRenderer.send("removeKeybind", keybindName);
});
});
sleep(3000).then(() => {
document.getElementById("warning")!.style.display = "none";
});

View file

@ -1,27 +1,28 @@
// Modules to control application life and create native browser window
import {BrowserWindow, app, crashReporter, session} from "electron";
import "v8-compile-cache";
import {
Settings,
checkForDataFolder,
checkIfConfigExists,
firstRun,
checkIfConfigIsBroken,
getConfig,
getConfigSync,
injectElectronFlags,
installModLoader,
setConfig,
setLang
} from "./utils";
import "./extensions/mods";
import "./discord/extensions/csp";
import "./tray";
import {createCustomWindow, createNativeWindow, createTransparentWindow} from "./window";
import fs from "fs";
import {createCustomWindow, createNativeWindow, createTransparentWindow} from "./discord/window";
import path from "path";
import {createTManagerWindow} from "./themeManager/main";
import {createSplashWindow} from "./splash/main";
import {createSetupWindow} from "./setup/main";
import {createKeybindWindow} from "./keybindMaker/main";
import {
setConfig,
getConfigSync,
checkForDataFolder,
checkIfConfigExists,
checkIfConfigIsBroken,
getConfig,
firstRun,
Settings,
getConfigLocation
} from "./common/config";
import {injectElectronFlags} from "./common/flags";
import {setLang} from "./common/lang";
import {installModLoader} from "./discord/extensions/mods";
export let iconPath: string;
export let settings: any;
export let customTitlebar: boolean;
@ -47,10 +48,6 @@ async function args(): Promise<void> {
app.whenReady().then(async () => {
createTManagerWindow();
});
} else if (args == "keybinds") {
app.whenReady().then(async () => {
createKeybindWindow();
});
}
}
args(); // i want my top level awaits
@ -84,6 +81,7 @@ if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false
checkIfConfigExists();
checkIfConfigIsBroken();
injectElectronFlags();
console.log("[Config Manager] Current config: " + fs.readFileSync(getConfigLocation(), "utf-8"));
app.whenReady().then(async () => {
if ((await getConfig("customIcon")) !== undefined ?? null) {
iconPath = await getConfig("customIcon");

View file

@ -1,7 +1,7 @@
import {BrowserWindow, app, shell} from "electron";
import {getDisplayVersion} from "../utils";
import path from "path";
import fs from "fs";
import {getDisplayVersion} from "../common/version";
let settingsWindow: BrowserWindow;
let instance = 0;

View file

@ -2,7 +2,8 @@ import {BrowserWindow, app, ipcMain} from "electron";
import path from "path";
import * as fs from "fs";
import {iconPath} from "../main";
import {Settings, getConfigLocation, setConfigBulk} from "../utils";
import {setConfigBulk, getConfigLocation, Settings} from "../common/config";
let setupWindow: BrowserWindow;
export function createSetupWindow(): void {
setupWindow = new BrowserWindow({

View file

@ -1,5 +1,6 @@
import {contextBridge, ipcRenderer} from "electron";
import {injectTitlebar} from "../preload/titlebar";
import {injectTitlebar} from "../discord/preload/titlebar";
injectTitlebar();
contextBridge.exposeInMainWorld("armcordinternal", {
restart: () => ipcRenderer.send("restart"),

View file

@ -19,5 +19,5 @@ export async function createSplashWindow(): Promise<void> {
preload: path.join(__dirname, "preload.js")
}
});
splashWindow.loadURL(`file://${__dirname}/splash.html`);
splashWindow.loadFile(path.join(__dirname, "splash.html"));
}

View file

@ -1,8 +1,8 @@
import {BrowserWindow, app, dialog, ipcMain, shell} from "electron";
import {sleep} from "../utils";
import path from "path";
import fs from "fs";
import {createInviteWindow, mainWindow} from "../window";
import {sleep} from "../common/sleep";
import {createInviteWindow, mainWindow} from "../discord/window";
let themeWindow: BrowserWindow;
let instance = 0;
interface ThemeManifest {

View file

@ -1,5 +1,5 @@
import {ipcRenderer, contextBridge} from "electron";
import {sleep} from "../utils";
import {sleep} from "../common/sleep";
contextBridge.exposeInMainWorld("themes", {
install: (url: string) => ipcRenderer.send("installBDTheme", url),
uninstall: (id: string) => ipcRenderer.send("uninstallTheme", id)

View file

@ -1,9 +1,10 @@
import * as fs from "fs";
import {Menu, MessageBoxOptions, Tray, app, dialog, nativeImage} from "electron";
import {createInviteWindow, mainWindow} from "./window";
import {getConfig, getConfigLocation, getDisplayVersion, setConfig} from "./utils";
import {createInviteWindow, mainWindow} from "./discord/window";
import * as path from "path";
import {createSettingsWindow} from "./settings/main";
import {getConfig, getConfigLocation, setConfig} from "./common/config";
import {getDisplayVersion} from "./common/version";
export let tray: any = null;
let trayIcon = "ac_plug_colored";
app.whenReady().then(async () => {

View file

@ -1,478 +0,0 @@
import * as fs from "fs";
import {app, dialog, globalShortcut} from "electron";
import path from "path";
import fetch from "cross-fetch";
import extract from "extract-zip";
import util from "util";
const streamPipeline = util.promisify(require("stream").pipeline);
export let firstRun: boolean;
export let contentPath: string;
export let transparency: boolean;
//utility functions that are used all over the codebase or just too obscure to be put in the file used in
export function addStyle(styleString: string): void {
const style = document.createElement("style");
style.textContent = styleString;
document.head.append(style);
}
export function addScript(scriptString: string): void {
let script = document.createElement("script");
script.textContent = scriptString;
document.body.append(script);
}
export async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function checkIfConfigIsBroken(): Promise<void> {
try {
let settingsData = fs.readFileSync(getConfigLocation(), "utf-8");
JSON.parse(settingsData);
console.log("Config is fine");
} catch (e) {
console.error(e);
console.log("Detected a corrupted config");
setup();
dialog.showErrorBox(
"Oops, something went wrong.",
"ArmCord has detected that your configuration file is corrupted, please restart the app and set your settings again. If this issue persists, report it on the support server/Github issues."
);
}
try {
let windowData = fs.readFileSync(getWindowStateLocation(), "utf-8");
JSON.parse(windowData);
console.log("Window config is fine");
} catch (e) {
console.error(e);
fs.writeFileSync(getWindowStateLocation(), "{}", "utf-8");
console.log("Detected a corrupted window config");
}
}
export function setup(): void {
console.log("Setting up temporary ArmCord settings.");
const defaults: Settings = {
windowStyle: "default",
channel: "stable",
armcordCSP: true,
minimizeToTray: true,
keybinds: [],
multiInstance: false,
mods: "none",
spellcheck: true,
performanceMode: "none",
skipSplash: false,
inviteWebsocket: true,
startMinimized: false,
dynamicIcon: false,
tray: true,
customJsBundle: "https://armcord.app/placeholder.js",
customCssBundle: "https://armcord.app/placeholder.css",
disableAutogain: false,
useLegacyCapturer: false,
mobileMode: false,
trayIcon: "default",
doneSetup: false,
clientName: "ArmCord",
customIcon: path.join(__dirname, "../", "/assets/desktop.png")
};
setConfigBulk({
...defaults
});
}
//Get the version value from the "package.json" file
export const packageVersion = require("../package.json").version;
export function getVersion(): string {
return packageVersion;
}
export function getDisplayVersion(): string {
//Checks if the app version # has 4 sections (3.1.0.0) instead of 3 (3.1.0) / Shitty way to check if Kernel Mod is installed
if ((app.getVersion() == packageVersion) == false) {
if ((app.getVersion() == process.versions.electron) == true) {
return `Dev Build (${packageVersion})`;
} else {
return `${packageVersion} [Modified]`;
}
} else {
return packageVersion;
}
}
export async function injectJS(inject: string): Promise<void> {
const js = await (await fetch(`${inject}`)).text();
const el = document.createElement("script");
el.appendChild(document.createTextNode(js));
document.body.appendChild(el);
}
export async function injectElectronFlags(): Promise<void> {
// MIT License
// Copyright (c) 2022 GooseNest
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
const presets = {
performance: `--enable-gpu-rasterization --enable-zero-copy --ignore-gpu-blocklist --enable-hardware-overlays=single-fullscreen,single-on-top,underlay --enable-features=EnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilation --disable-features=Vulkan --force_high_performance_gpu`, // Performance
battery: "--enable-features=TurnOffStreamingMediaCachingOnBattery --force_low_power_gpu", // Known to have better battery life for Chromium?
vaapi: "--ignore-gpu-blocklist --enable-features=VaapiVideoDecoder --enable-gpu-rasterization --enable-zero-copy --force_high_performance_gpu --use-gl=desktop --disable-features=UseChromeOSDirectVideoDecoder"
};
switch (await getConfig("performanceMode")) {
case "performance":
console.log("Performance mode enabled");
app.commandLine.appendArgument(presets.performance);
break;
case "battery":
console.log("Battery mode enabled");
app.commandLine.appendArgument(presets.battery);
break;
default:
console.log("No performance modes set");
}
if ((await getConfig("windowStyle")) == "transparent" && process.platform === "win32") {
transparency = true;
}
}
export async function setLang(language: string): Promise<void> {
const langConfigFile = `${path.join(app.getPath("userData"), "/storage/")}lang.json`;
if (!fs.existsSync(langConfigFile)) {
fs.writeFileSync(langConfigFile, "{}", "utf-8");
}
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
parsed.lang = language;
let toSave = JSON.stringify(parsed, null, 4);
fs.writeFileSync(langConfigFile, toSave, "utf-8");
}
let language: string;
export async function getLang(object: string): Promise<string> {
if (language == undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
language = parsed.lang;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
language = "en-US";
}
}
if (language.length == 2) {
language = `${language}-${language.toUpperCase()}`;
}
let langPath = path.join(__dirname, "../", `/assets/lang/${language}.json`);
if (!fs.existsSync(langPath)) {
langPath = path.join(__dirname, "../", "/assets/lang/en-US.json");
}
let rawdata = fs.readFileSync(langPath, "utf-8");
let parsed = JSON.parse(rawdata);
if (parsed[object] == undefined) {
console.log(`${object} is undefined in ${language}`);
langPath = path.join(__dirname, "../", "/assets/lang/en-US.json");
rawdata = fs.readFileSync(langPath, "utf-8");
parsed = JSON.parse(rawdata);
return parsed[object];
} else {
return parsed[object];
}
}
export async function getLangName(): Promise<string> {
if (language == undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
language = parsed.lang;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
language = "en-US";
}
}
if (language.length == 2) {
language = `${language}-${language.toUpperCase()}`;
}
return language;
}
//ArmCord Window State manager
export interface WindowState {
width: number;
height: number;
x: number;
y: number;
isMaximized: boolean;
}
function getWindowStateLocation() {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}window.json`;
}
export async function setWindowState(object: WindowState): Promise<void> {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const saveFile = `${storagePath}window.json`;
let toSave = JSON.stringify(object, null, 4);
fs.writeFileSync(saveFile, toSave, "utf-8");
}
export async function getWindowState<K extends keyof WindowState>(object: K): Promise<WindowState[K]> {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}window.json`;
if (!fs.existsSync(settingsFile)) {
fs.writeFileSync(settingsFile, "{}", "utf-8");
}
let rawdata = fs.readFileSync(settingsFile, "utf-8");
let returndata = JSON.parse(rawdata);
console.log(`[Window state manager] ${returndata}`);
return returndata[object];
}
//ArmCord Settings/Storage manager
export function checkForDataFolder(): void {
const dataPath = path.join(path.dirname(app.getPath("exe")), "armcord-data");
if (fs.existsSync(dataPath) && fs.statSync(dataPath).isDirectory()) {
console.log("Found armcord-data folder. Running in portable mode.");
app.setPath("userData", dataPath);
}
}
export interface Settings {
// Referenced for detecting a broken config.
"0"?: string;
// Referenced once for disabling mod updating.
noBundleUpdates?: boolean;
// Only used for external url warning dialog.
ignoreProtocolWarning?: boolean;
customIcon: string;
windowStyle: string;
channel: string;
armcordCSP: boolean;
minimizeToTray: boolean;
multiInstance: boolean;
spellcheck: boolean;
mods: string;
dynamicIcon: boolean;
mobileMode: boolean;
skipSplash: boolean;
performanceMode: string;
customJsBundle: RequestInfo | URL;
customCssBundle: RequestInfo | URL;
startMinimized: boolean;
useLegacyCapturer: boolean;
tray: boolean;
keybinds: Array<string>;
inviteWebsocket: boolean;
disableAutogain: boolean;
trayIcon: string;
doneSetup: boolean;
clientName: string;
}
export function getConfigLocation(): string {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}settings.json`;
}
export async function getConfig<K extends keyof Settings>(object: K): Promise<Settings[K]> {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let returndata = JSON.parse(rawdata);
console.log(`[Config manager] ${object}: ${returndata[object]}`);
return returndata[object];
}
export function getConfigSync<K extends keyof Settings>(object: K) {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let returndata = JSON.parse(rawdata);
console.log(`[Config manager] ${object}: ${returndata[object]}`);
return returndata[object];
}
export async function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): Promise<void> {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let parsed = JSON.parse(rawdata);
parsed[object] = toSet;
let toSave = JSON.stringify(parsed, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
}
export async function setConfigBulk(object: Settings): Promise<void> {
let existingData = {};
try {
const existingDataBuffer = fs.readFileSync(getConfigLocation(), "utf-8");
existingData = JSON.parse(existingDataBuffer.toString());
} catch (error) {
// Ignore errors when the file doesn't exist or parsing fails
}
// Merge the existing data with the new data
const mergedData = {...existingData, ...object};
// Write the merged data back to the file
const toSave = JSON.stringify(mergedData, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
}
export async function checkIfConfigExists(): Promise<void> {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}settings.json`;
if (!fs.existsSync(settingsFile)) {
if (!fs.existsSync(storagePath)) {
fs.mkdirSync(storagePath);
console.log("Created missing storage folder");
}
console.log("First run of the ArmCord. Starting setup.");
setup();
firstRun = true;
} else if ((await getConfig("doneSetup")) == false) {
console.log("First run of the ArmCord. Starting setup.");
setup();
firstRun = true;
} else {
console.log("ArmCord has been run before. Skipping setup.");
}
}
// Mods
async function updateModBundle(): Promise<void> {
if ((await getConfig("noBundleUpdates")) == undefined ?? false) {
try {
console.log("Downloading mod bundle");
const distFolder = `${app.getPath("userData")}/plugins/loader/dist/`;
while (!fs.existsSync(distFolder)) {
//waiting
}
let name: string = await getConfig("mods");
if (name == "custom") {
// aspy fix
let bundle: string = await (await fetch(await getConfig("customJsBundle"))).text();
fs.writeFileSync(`${distFolder}bundle.js`, bundle, "utf-8");
let css: string = await (await fetch(await getConfig("customCssBundle"))).text();
fs.writeFileSync(`${distFolder}bundle.css`, css, "utf-8");
} else {
const clientMods = {
vencord: "https://github.com/Vendicated/Vencord/releases/download/devbuild/browser.js",
shelter: "https://raw.githubusercontent.com/uwu/shelter-builds/main/shelter.js"
};
const clientModsCss = {
vencord: "https://github.com/Vendicated/Vencord/releases/download/devbuild/browser.css",
shelter: "https://armcord.app/placeholder.css"
};
console.log(clientMods[name as keyof typeof clientMods]);
let bundle: string = await (await fetch(clientMods[name as keyof typeof clientMods])).text();
fs.writeFileSync(`${distFolder}bundle.js`, bundle, "utf-8");
let css: string = await (await fetch(clientModsCss[name as keyof typeof clientModsCss])).text();
fs.writeFileSync(`${distFolder}bundle.css`, css, "utf-8");
}
} catch (e) {
console.log("[Mod loader] Failed to install mods");
console.error(e);
dialog.showErrorBox(
"Oops, something went wrong.",
"ArmCord couldn't install mods, please check if you have stable internet connection and restart the app. If this issue persists, report it on the support server/Github issues."
);
}
} else {
console.log("[Mod loader] Skipping mod bundle update");
}
}
export let modInstallState: string;
export function updateModInstallState() {
modInstallState = "modDownload";
updateModBundle();
import("./extensions/plugin");
modInstallState = "done";
}
export async function installModLoader(): Promise<void> {
if ((await getConfig("mods")) == "none") {
modInstallState = "none";
fs.rmSync(`${app.getPath("userData")}/plugins/loader`, {recursive: true, force: true});
import("./extensions/plugin");
console.log("[Mod loader] Skipping");
return;
}
const pluginFolder = `${app.getPath("userData")}/plugins/`;
if (fs.existsSync(`${pluginFolder}loader`) && fs.existsSync(`${pluginFolder}loader/dist/bundle.css`)) {
updateModInstallState();
return;
}
try {
fs.rmSync(`${app.getPath("userData")}/plugins/loader`, {recursive: true, force: true});
modInstallState = "installing";
let zipPath = `${app.getPath("temp")}/loader.zip`;
if (!fs.existsSync(pluginFolder)) {
fs.mkdirSync(pluginFolder);
console.log("[Mod loader] Created missing plugin folder");
}
// Add more of these later if needed!
let URLs = [
"https://armcord.app/loader.zip",
"https://armcord.vercel.app/loader.zip",
"https://raw.githubusercontent.com/ArmCord/website/new/public/loader.zip"
];
let loaderZip: any;
while (true) {
if (URLs.length <= 0) throw new Error(`unexpected response ${loaderZip.statusText}`);
try {
loaderZip = await fetch(URLs[0]);
} catch (err) {
console.log("[Mod loader] Failed to download. Links left to try: " + (URLs.length - 1));
URLs.splice(0, 1);
continue;
}
break;
}
await streamPipeline(loaderZip.body, fs.createWriteStream(zipPath));
await extract(zipPath, {dir: path.join(app.getPath("userData"), "plugins")});
updateModInstallState();
} catch (e) {
console.log("[Mod loader] Failed to install modloader");
console.error(e);
dialog.showErrorBox(
"Oops, something went wrong.",
"ArmCord couldn't install internal mod loader, please check if you have stable internet connection and restart the app. If this issue persists, report it on the support server/Github issues."
);
}
}
export async function registerGlobalKeybinds(): Promise<void> {
const keybinds = await getConfig("keybinds");
keybinds.forEach((keybind) => {
globalShortcut.register(keybind, () => {
console.log(keybind);
});
});
}

View file

@ -1,5 +1,5 @@
// Reference: https://www.typescriptlang.org/tsconfig
{
// Reference: https://www.typescriptlang.org/tsconfig
"include": ["src/**/*"], // This makes it so that the compiler won't compile anything outside of "src".
//"exclude": ["src/**/*.test.ts"], // Exclude .test.ts files since they're for Jest only.
"compilerOptions": {