Add theme manager

This commit is contained in:
smartfrigde 2023-05-13 22:09:01 +02:00
parent 9ff515b07b
commit a07546f28e
12 changed files with 496 additions and 11 deletions

View file

@ -29,3 +29,9 @@
[class|="listItem"]:has(+ [class|="listItem"] [data-list-item-id="guildsnav___app-download-button"]) {
display: none;
}
div#themes:after,
div#armcord:after {
content: url("https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/ac_white_plug16x.png");
margin-right: 5px;
}

View file

@ -36,7 +36,7 @@
.titlebar #window-controls-container #quit {
float: left;
height: 100%;
width: 33.1%;
width: 33.3%;
text-align: center;
color: var(--interactive-normal);
cursor: default;

View file

@ -16,6 +16,7 @@ import {customTitlebar} from "./main";
import {createSettingsWindow} from "./settings/main";
import os from "os";
import path from "path";
import {createTManagerWindow} from "./themeManager/main";
export function registerIpc(): void {
ipcMain.on("get-app-path", (event) => {
event.reply("app-path", app.getAppPath());
@ -142,6 +143,9 @@ export function registerIpc(): void {
ipcMain.on("openSettingsWindow", () => {
createSettingsWindow();
});
ipcMain.on("openManagerWindow", () => {
createTManagerWindow();
});
ipcMain.on("setting-armcordCSP", async (event) => {
if (await getConfig("armcordCSP")) {
event.returnValue = true;

View file

@ -59,6 +59,13 @@ export async function setMenu(): Promise<void> {
createSettingsWindow();
}
},
{
label: "Fullscreen",
accelerator: "F11",
click() {
mainWindow.fullScreen = !mainWindow.fullScreen;
}
},
{
label: "Reload",
accelerator: "CmdOrCtrl+R",

9
src/preload/optimizer.ts Normal file
View file

@ -0,0 +1,9 @@
const optimize = (orig: Function) =>
function (this: any, ...args: any[]) {
if (typeof args[0]?.className === "string" && args[0].className.indexOf("activity") !== -1)
return setTimeout(() => orig.apply(this, args), 100);
return orig.apply(this, args);
};
Element.prototype.removeChild = optimize(Element.prototype.removeChild);

View file

@ -1,5 +1,6 @@
import "./bridge";
import "./patch";
import "./optimizer";
import {ipcRenderer} from "electron";
import * as fs from "fs";
@ -78,12 +79,33 @@ if (window.location.href.indexOf("splash.html") > -1) {
// Settings info version injection
setInterval(() => {
const host = document.querySelector("nav > [class|=side] [class|=info]");
if (!host || host.querySelector("#ac-ver")) return;
const el = host.firstChild!.cloneNode() as HTMLSpanElement;
el.id = "ac-ver";
const host = document.querySelector<HTMLDivElement>("nav > [class|=side] [class|=info]");
if (!host || host.querySelector("#ac-ver")) {
return;
}
const el = host.firstElementChild!.cloneNode() as HTMLSpanElement;
el.id = "ac-ver";
el.textContent = `ArmCord Version: ${version}`;
el.onclick = () => ipcRenderer.send("openSettingsWindow");
host.append(el);
}, 2000);
let advanced = document
.querySelector('[class*="socialLinks-"]')!
.parentElement!.querySelector(
'[class*="header"] + [class*="item"] + [class*="item"] + [class*="item"] + [class*="item"] + [class*="item"] + [class*="item"] + [class*="item"] + [class*="item"] + [class*="item"]'
);
if (!advanced) return;
if (advanced.nextSibling instanceof Element && advanced.nextSibling.className.includes("item")) {
advanced = advanced.nextSibling;
}
const acSettings = advanced.cloneNode(true) as HTMLElement;
const tManager = advanced.cloneNode(true) as HTMLElement;
acSettings.textContent = "ArmCord";
acSettings.id = "armcord";
acSettings.onclick = () => ipcRenderer.send("openSettingsWindow");
tManager.textContent = "Themes";
tManager.id = "themes";
tManager.onclick = () => ipcRenderer.send("openManagerWindow");
advanced.insertAdjacentElement("afterend", acSettings);
advanced.insertAdjacentElement("afterend", tManager);
}, 1000);

View file

@ -50,6 +50,9 @@ export function createSettingsWindow(): void {
fs.mkdirSync(themesFolder);
console.log("Created missing theme folder");
}
if (!fs.existsSync(`${userDataPath}/disabled.txt`)) {
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), "");
}
settingsWindow.webContents.on("did-finish-load", () => {
fs.readdirSync(themesFolder).forEach((file) => {
try {

181
src/themeManager/main.ts Normal file
View file

@ -0,0 +1,181 @@
import {BrowserWindow, app, ipcMain, shell, dialog} from "electron";
import {sleep} from "../utils";
import path from "path";
import fs from "fs";
import {mainWindow} from "../window";
let themeWindow: BrowserWindow;
let instance = 0;
interface ThemeManifest {
name?: string;
author?: string;
description?: string;
version?: string;
invite?: string;
authorId?: string;
theme: string;
authorLink?: string;
donate?: string;
patreon?: string;
website?: string;
source?: string;
}
function parseBDManifest(content: string) {
const metaReg = /@([^ ]*) (.*)/g;
if (!content.startsWith("/**")) {
throw new Error("Not a manifest.");
}
let manifest: ThemeManifest = {theme: "src.css"};
let match;
while ((match = metaReg.exec(content)) !== null) {
let [_, key, value] = match;
if (key === "import") break;
value = value.trim();
//console.log(key, value);
switch (key) {
case "name":
manifest.name = value;
break;
case "description":
manifest.description = value;
break;
case "version":
manifest.version = value;
break;
case "author":
manifest.author = value;
break;
case "invite":
manifest.invite = value;
break;
case "authorId":
manifest.authorId = value;
break;
case "authorLink":
manifest.authorLink = value;
break;
case "donate":
manifest.donate = value;
break;
case "patreon":
manifest.patreon = value;
break;
case "website":
manifest.website = value;
break;
case "source":
manifest.source = value;
break;
}
}
return manifest;
}
const userDataPath = app.getPath("userData");
const themesPath = path.join(userDataPath, "/themes/");
export function createTManagerWindow(): void {
console.log("Creating theme manager window.");
instance += 1;
if (instance > 1) {
if (themeWindow) {
themeWindow.show();
themeWindow.restore();
}
} else {
themeWindow = new BrowserWindow({
width: 700,
height: 600,
title: `ArmCord Theme Manager`,
darkTheme: true,
frame: true,
backgroundColor: "#2f3136",
autoHideMenuBar: true,
webPreferences: {
sandbox: false,
preload: path.join(__dirname, "preload.js")
}
});
async function managerLoadPage(): Promise<void> {
themeWindow.loadFile(`${__dirname}/manager.html`);
}
const userDataPath = app.getPath("userData");
const themesFolder = `${userDataPath}/themes/`;
if (!fs.existsSync(themesFolder)) {
fs.mkdirSync(themesFolder);
console.log("Created missing theme folder");
}
if (!fs.existsSync(`${userDataPath}/disabled.txt`)) {
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), "");
}
ipcMain.on("openThemesFolder", async () => {
shell.showItemInFolder(themesPath);
await sleep(1000);
});
ipcMain.on("reloadMain", async () => {
mainWindow.webContents.reload();
});
ipcMain.on("addToDisabled", async (_event, name: string) => {
fs.appendFileSync(path.join(userDataPath, "/disabled.txt"), name + "\n");
});
ipcMain.on("disabled", async (e) => {
e.returnValue = fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString();
});
ipcMain.on("removeFromDisabled", async (_event, name: string) => {
var e = await fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString();
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), e.replace(name + "\n", ""));
});
ipcMain.on("installBDTheme", async (_event, link: string) => {
try {
var code = await (await fetch(link)).text();
var manifest = parseBDManifest(code);
var themePath = path.join(themesFolder, manifest.name?.replace(" ", "-") + "-BD");
if (!fs.existsSync(themePath)) {
fs.mkdirSync(themePath);
console.log(`Created ${manifest.name} folder`);
}
fs.writeFileSync(path.join(themePath, "manifest.json"), JSON.stringify(manifest));
fs.writeFileSync(path.join(themePath, "src.css"), code);
dialog.showMessageBoxSync({
type: "info",
title: "BD Theme import success",
message: "Successfully imported theme from link."
});
themeWindow.webContents.reload();
mainWindow.webContents.reload();
} catch (e) {
dialog.showErrorBox(
"BD Theme import fail",
"Failed to import theme from link. Please make sure that it's a valid BetterDiscord Theme."
);
}
});
themeWindow.webContents.on("did-finish-load", () => {
fs.readdirSync(themesFolder).forEach((file) => {
try {
const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8");
console.log(manifest);
themeWindow.webContents.send("themeManifest", manifest);
} catch (err) {
console.error(err);
}
});
});
managerLoadPage();
themeWindow.on("close", () => {
instance = 0;
});
}
}

View file

@ -0,0 +1,198 @@
<!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.xyz/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;
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;
}
</style>
</head>
<body>
<div class="flex-box">
<input
type="text"
id="themeLink"
name="themeLink"
title="BetterDiscord theme format"
placeholder="https://raw.githubusercontent.com/... [.css]"
/>
<img id="download" src="https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/Download.png" />
</div>
<br />
<div class="cards" id="cardBox"></div>
</body>
</html>

View file

@ -0,0 +1,43 @@
import {ipcRenderer} from "electron";
import {sleep} from "../utils";
ipcRenderer.on("themeManifest", (_event, json) => {
var manifest = JSON.parse(json);
console.log(manifest);
sleep(1000);
var e = document.getElementById("cardBox");
e?.insertAdjacentHTML(
"beforeend",
`
<div class="card">
<div class="flex-box">
<h3>${manifest.name}</h3>
<input id="${manifest.name.replace(" ", "-")}" class="tgl tgl-light left" type="checkbox" />
<label class="tgl-btn left" for="${manifest.name.replace(" ", "-")}"></label>
</div>
<p>${manifest.description}</p>
</div>
`
);
if (!ipcRenderer.sendSync("disabled").includes(manifest.name.replace(" ", "-"))) {
(<HTMLInputElement>document.getElementById(manifest.name.replace(" ", "-"))).checked = true;
}
(<HTMLInputElement>document.getElementById(manifest.name.replace(" ", "-")))!.addEventListener(
"input",
function (evt) {
ipcRenderer.send("reloadMain");
if (this.checked) {
ipcRenderer.send("removeFromDisabled", manifest.name.replace(" ", "-"));
} else {
ipcRenderer.send("addToDisabled", manifest.name.replace(" ", "-"));
}
}
);
});
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("download")!.addEventListener("click", () => {
ipcRenderer.send("installBDTheme", (<HTMLInputElement>document.getElementById("themeLink"))!.value);
});
});

View file

@ -179,16 +179,28 @@ async function doAfterDefiningTheWindow(): Promise<void> {
fs.mkdirSync(themesFolder);
console.log("Created missing theme folder");
}
if (!fs.existsSync(`${userDataPath}/disabled.txt`)) {
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), "");
}
mainWindow.webContents.on("did-finish-load", () => {
fs.readdirSync(themesFolder).forEach((file) => {
try {
const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8");
let themeFile = JSON.parse(manifest);
mainWindow.webContents.send(
"themeLoader",
fs.readFileSync(`${themesFolder}/${file}/${themeFile.theme}`, "utf-8")
);
console.log(`%cLoaded ${themeFile.name} made by ${themeFile.author}`, "color:red");
if (
fs
.readFileSync(path.join(userDataPath, "/disabled.txt"))
.toString()
.includes(themeFile.name.replace(" ", "-"))
) {
console.log(`%cSkipped ${themeFile.name} made by ${themeFile.author}`, "color:red");
} else {
mainWindow.webContents.send(
"themeLoader",
fs.readFileSync(`${themesFolder}/${file}/${themeFile.theme}`, "utf-8")
);
console.log(`%cLoaded ${themeFile.name} made by ${themeFile.author}`, "color:red");
}
} catch (err) {
console.error(err);
}