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 electron from "electron";
import {getConfig} from "../utils"; import {getConfig} from "../../common/config";
const unstrictCSP = (): void => { const unstrictCSP = (): void => {
console.log("Setting up CSP unstricter..."); 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 //ipc stuff
import {app, clipboard, desktopCapturer, ipcMain, nativeImage, shell} from "electron"; import {app, clipboard, desktopCapturer, ipcMain, nativeImage, shell} from "electron";
import {mainWindow} from "./window"; 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 os from "os";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import {createTManagerWindow} from "./themeManager/main"; import {getConfig, setConfigBulk, getConfigLocation, Settings} from "../common/config";
import {splashWindow} from "./splash/main"; import {setLang, getLang, getLangName} from "../common/lang";
import {createKeybindWindow} from "./keybindMaker/main"; 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 userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
const themesPath = path.join(userDataPath, "/themes/"); const themesPath = path.join(userDataPath, "/themes/");
@ -136,9 +128,6 @@ export function registerIpc(): void {
ipcMain.on("openManagerWindow", () => { ipcMain.on("openManagerWindow", () => {
createTManagerWindow(); createTManagerWindow();
}); });
ipcMain.on("openKeybindWindow", () => {
createKeybindWindow();
});
ipcMain.on("setting-armcordCSP", async (event) => { ipcMain.on("setting-armcordCSP", async (event) => {
if (await getConfig("armcordCSP")) { if (await getConfig("armcordCSP")) {
event.returnValue = true; event.returnValue = true;

View file

@ -1,6 +1,6 @@
import {BrowserWindow, Menu, app, clipboard} from "electron"; import {BrowserWindow, Menu, app, clipboard} from "electron";
import {mainWindow} from "./window"; import {mainWindow} from "./window";
import {createSettingsWindow} from "./settings/main"; import {createSettingsWindow} from "../settings/main";
function paste(contents: any): void { function paste(contents: any): void {
const contentTypes = clipboard.availableFormats().toString(); 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 //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 //original https://github.com/electron/electron/issues/16513#issuecomment-602070250
import {addScript, addStyle} from "../utils";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import {addScript, addStyle} from "../../common/dom";
const CANCEL_ID = "desktop-capturer-selection__cancel"; 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 fs from "fs";
import * as path from "path"; import * as path from "path";
export function injectMobileStuff(): void { export function injectMobileStuff(): void {

View file

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

View file

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

View file

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

View file

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

View file

@ -4,22 +4,15 @@
// I'm sorry for this mess but I'm not sure how to fix it. // 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 {BrowserWindow, MessageBoxOptions, app, dialog, nativeImage, shell} from "electron";
import path from "path"; import path from "path";
import {
contentPath,
firstRun,
getConfig,
getWindowState,
registerGlobalKeybinds,
setConfig,
setWindowState
} from "./utils";
import {registerIpc} from "./ipc"; import {registerIpc} from "./ipc";
import {setMenu} from "./menu"; import {setMenu} from "./menu";
import * as fs from "fs"; import * as fs from "fs";
import contextMenu from "electron-context-menu"; import contextMenu from "electron-context-menu";
import os from "os"; import os from "os";
import {tray} from "./tray"; import {tray} from "../tray";
import {iconPath} from "./main"; import {iconPath} from "../main";
import {getConfig, setConfig, firstRun} from "../common/config";
import {getWindowState, setWindowState} from "../common/windowState";
export let mainWindow: BrowserWindow; export let mainWindow: BrowserWindow;
export let inviteWindow: BrowserWindow; export let inviteWindow: BrowserWindow;
let forceQuit = false; let forceQuit = false;
@ -196,7 +189,6 @@ async function doAfterDefiningTheWindow(): Promise<void> {
if (!fs.existsSync(`${userDataPath}/disabled.txt`)) { if (!fs.existsSync(`${userDataPath}/disabled.txt`)) {
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), ""); fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), "");
} }
registerGlobalKeybinds();
mainWindow.webContents.on("did-finish-load", () => { mainWindow.webContents.on("did-finish-load", () => {
fs.readdirSync(themesFolder).forEach((file) => { fs.readdirSync(themesFolder).forEach((file) => {
try { try {
@ -265,7 +257,6 @@ async function doAfterDefiningTheWindow(): Promise<void> {
mainWindow.on("unmaximize", () => { mainWindow.on("unmaximize", () => {
mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`); mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`);
}); });
console.log(contentPath);
if ((await getConfig("inviteWebsocket")) == true) { if ((await getConfig("inviteWebsocket")) == true) {
require("arrpc"); require("arrpc");
//await startServer(); //await startServer();
@ -274,7 +265,7 @@ async function doAfterDefiningTheWindow(): Promise<void> {
mainWindow.close(); mainWindow.close();
} }
//loadURL broke for no good reason after E28 //loadURL broke for no good reason after E28
mainWindow.loadFile(`${__dirname}/splash/redirect.html`); mainWindow.loadFile(`${__dirname}/../splash/redirect.html`);
if (await getConfig("skipSplash")) { if (await getConfig("skipSplash")) {
mainWindow.show(); 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 // Modules to control application life and create native browser window
import {BrowserWindow, app, crashReporter, session} from "electron"; import {BrowserWindow, app, crashReporter, session} from "electron";
import "v8-compile-cache"; import "v8-compile-cache";
import { import "./discord/extensions/csp";
Settings,
checkForDataFolder,
checkIfConfigExists,
firstRun,
checkIfConfigIsBroken,
getConfig,
getConfigSync,
injectElectronFlags,
installModLoader,
setConfig,
setLang
} from "./utils";
import "./extensions/mods";
import "./tray"; import "./tray";
import {createCustomWindow, createNativeWindow, createTransparentWindow} from "./window"; import fs from "fs";
import {createCustomWindow, createNativeWindow, createTransparentWindow} from "./discord/window";
import path from "path"; import path from "path";
import {createTManagerWindow} from "./themeManager/main"; import {createTManagerWindow} from "./themeManager/main";
import {createSplashWindow} from "./splash/main"; import {createSplashWindow} from "./splash/main";
import {createSetupWindow} from "./setup/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 iconPath: string;
export let settings: any; export let settings: any;
export let customTitlebar: boolean; export let customTitlebar: boolean;
@ -47,10 +48,6 @@ async function args(): Promise<void> {
app.whenReady().then(async () => { app.whenReady().then(async () => {
createTManagerWindow(); createTManagerWindow();
}); });
} else if (args == "keybinds") {
app.whenReady().then(async () => {
createKeybindWindow();
});
} }
} }
args(); // i want my top level awaits args(); // i want my top level awaits
@ -84,6 +81,7 @@ if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false
checkIfConfigExists(); checkIfConfigExists();
checkIfConfigIsBroken(); checkIfConfigIsBroken();
injectElectronFlags(); injectElectronFlags();
console.log("[Config Manager] Current config: " + fs.readFileSync(getConfigLocation(), "utf-8"));
app.whenReady().then(async () => { app.whenReady().then(async () => {
if ((await getConfig("customIcon")) !== undefined ?? null) { if ((await getConfig("customIcon")) !== undefined ?? null) {
iconPath = await getConfig("customIcon"); iconPath = await getConfig("customIcon");

View file

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

View file

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

View file

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

View file

@ -19,5 +19,5 @@ export async function createSplashWindow(): Promise<void> {
preload: path.join(__dirname, "preload.js") 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 {BrowserWindow, app, dialog, ipcMain, shell} from "electron";
import {sleep} from "../utils";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import {createInviteWindow, mainWindow} from "../window"; import {sleep} from "../common/sleep";
import {createInviteWindow, mainWindow} from "../discord/window";
let themeWindow: BrowserWindow; let themeWindow: BrowserWindow;
let instance = 0; let instance = 0;
interface ThemeManifest { interface ThemeManifest {

View file

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

View file

@ -1,9 +1,10 @@
import * as fs from "fs"; import * as fs from "fs";
import {Menu, MessageBoxOptions, Tray, app, dialog, nativeImage} from "electron"; import {Menu, MessageBoxOptions, Tray, app, dialog, nativeImage} from "electron";
import {createInviteWindow, mainWindow} from "./window"; import {createInviteWindow, mainWindow} from "./discord/window";
import {getConfig, getConfigLocation, getDisplayVersion, setConfig} from "./utils";
import * as path from "path"; import * as path from "path";
import {createSettingsWindow} from "./settings/main"; import {createSettingsWindow} from "./settings/main";
import {getConfig, getConfigLocation, setConfig} from "./common/config";
import {getDisplayVersion} from "./common/version";
export let tray: any = null; export let tray: any = null;
let trayIcon = "ac_plug_colored"; let trayIcon = "ac_plug_colored";
app.whenReady().then(async () => { 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". "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. //"exclude": ["src/**/*.test.ts"], // Exclude .test.ts files since they're for Jest only.
"compilerOptions": { "compilerOptions": {