mirror of
				https://github.com/smartfrigde/armcord.git
				synced 2024-08-14 23:56:58 +00:00 
			
		
		
		
	feat: new folder structure
This commit is contained in:
		
							parent
							
								
									8aca371346
								
							
						
					
					
						commit
						419cb8eb4a
					
				
					 48 changed files with 536 additions and 1059 deletions
				
			
		
							
								
								
									
										157
									
								
								src/common/config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/common/config.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										20
									
								
								src/common/dom.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										47
									
								
								src/common/flags.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										67
									
								
								src/common/lang.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										3
									
								
								src/common/sleep.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										19
									
								
								src/common/version.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										34
									
								
								src/common/windowState.ts
									
										
									
									
									
										Normal 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]; | ||||
| } | ||||
|  | @ -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..."); | ||||
							
								
								
									
										124
									
								
								src/discord/extensions/mods.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/discord/extensions/mods.ts
									
										
									
									
									
										Normal 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." | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
|  | @ -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(); | ||||
|  | @ -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"; | ||||
| 
 | ||||
|  | @ -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 { | ||||
|  | @ -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); | ||||
|  | @ -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; | ||||
|  | @ -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", ""); | ||||
|  | @ -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 = { | ||||
|  | @ -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(); | ||||
|  | @ -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; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | @ -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> | ||||
|  | @ -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"; | ||||
| }); | ||||
							
								
								
									
										38
									
								
								src/main.ts
									
										
									
									
									
								
							
							
						
						
									
										38
									
								
								src/main.ts
									
										
									
									
									
								
							|  | @ -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"); | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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({ | ||||
|  |  | |||
|  | @ -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"), | ||||
|  |  | |||
|  | @ -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")); | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 () => { | ||||
|  |  | |||
							
								
								
									
										478
									
								
								src/utils.ts
									
										
									
									
									
								
							
							
						
						
									
										478
									
								
								src/utils.ts
									
										
									
									
									
								
							|  | @ -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); | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | @ -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": { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue