Skip to content

Commit

Permalink
feat(themes): Online Themes CSS Override
Browse files Browse the repository at this point in the history
  • Loading branch information
yuna0x0 committed Nov 15, 2024
1 parent 25ceff5 commit b59f39e
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 12 deletions.
2 changes: 2 additions & 0 deletions src/api/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface Settings {
enableReactDevtools: boolean;
themeLinks: string[];
enabledThemes: string[];
onlineThemeOverrides: Record<string, string>;
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
Expand Down Expand Up @@ -82,6 +83,7 @@ const DefaultSettings: Settings = {
useQuickCss: true,
themeLinks: [],
enabledThemes: [],
onlineThemeOverrides: {},
enableReactDevtools: false,
frameless: false,
transparent: false,
Expand Down
169 changes: 160 additions & 9 deletions src/components/VencordSettings/ThemesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Margins } from "@utils/margins";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { findLazy } from "@webpack";
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, TextInput, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";

import Plugins from "~plugins";
Expand All @@ -48,25 +48,173 @@ const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue &

const cl = classNameFactory("vc-settings-theme-");

function Validator({ link }: { link: string; }) {
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
async function fetchOnlineThemes(link: string) {
return fetch(link).then(res => {
if (res.status > 300) throw `${res.status} ${res.statusText}`;
const contentType = res.headers.get("Content-Type");
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
throw "Not a CSS file. Remember to use the raw link!";

return "Okay!";
}));
return res.text();
});
}

function OnlineThemeOverride({ rawCssText, rawLink }: { rawCssText: string; rawLink: string; }) {
const settings = useSettings();
const [cssRules, setCssRules] = useState<CSSRuleList | null>(null);
const [overrides, setOverrides] = useState<Record<string, Record<string, string>>>({});

useEffect(() => {
const sheet = new CSSStyleSheet();
sheet.replaceSync(rawCssText);
setCssRules(sheet.cssRules);

// Initialize overrides from existing settings using rawLink as key
if (settings.onlineThemeOverrides[rawLink]) {
const tempSheet = new CSSStyleSheet();
tempSheet.replaceSync(settings.onlineThemeOverrides[rawLink]);

const newOverrides: Record<string, Record<string, string>> = {};
Array.from(tempSheet.cssRules).forEach(rule => {
if (!(rule instanceof CSSStyleRule)) return;
const selector = rule.selectorText;
newOverrides[selector] = {};

for (const prop of rule.style) {
const value = rule.style.getPropertyValue(prop);
const priority = rule.style.getPropertyPriority(prop);
if (value) {
newOverrides[selector][prop] = `${value}${priority ? " !" + priority : ""}`;
}
}
});
setOverrides(newOverrides);
}
}, [rawCssText]);

const updateOverrides = (selector: string, property: string, value: string) => {
setOverrides(prev => {
const newOverrides = { ...prev };

// If the value is empty, remove the property
if (!value.trim()) {
if (newOverrides[selector]) {
delete newOverrides[selector][property];
// If no properties left, remove the selector
if (Object.keys(newOverrides[selector]).length === 0) {
delete newOverrides[selector];
}
}
} else {
// Add/update the property
if (!newOverrides[selector]) {
newOverrides[selector] = {};
}
newOverrides[selector][property] = value;
}

// Update settings using rawLink as key
if (Object.keys(newOverrides).length === 0) {
delete settings.onlineThemeOverrides[rawLink];
settings.onlineThemeOverrides = { ...settings.onlineThemeOverrides };
} else {
const sheet = new CSSStyleSheet();
Object.entries(newOverrides).forEach(([sel, props]) => {
const rule = `${sel} { ${Object.entries(props)
.map(([p, v]) => `${p}: ${v}`)
.join("; ")} }`;
try {
sheet.insertRule(rule);
} catch (e) {
console.error("Failed to insert rule:", rule, e);
}
});
settings.onlineThemeOverrides[rawLink] = Array.from(sheet.cssRules).map(rule => rule.cssText).join(" ");
settings.onlineThemeOverrides = { ...settings.onlineThemeOverrides };
}
return newOverrides;
});
};

if (!cssRules) return null;

return (
<Forms.FormSection className={Margins.top8}>
{Array.from(cssRules || []).map((rule, index) => {
if (!(rule instanceof CSSStyleRule)) return null;

const selector = rule.selectorText;
const properties: [string, string][] = [];

for (const prop of rule.style) {
const value = rule.style.getPropertyValue(prop);
const priority = rule.style.getPropertyPriority(prop);
if (value) {
properties.push([prop, `${value}${priority ? " !" + priority : ""}`]);
}
}

return (
<Card key={index} style={{ marginBottom: ".5em", paddingTop: ".2em", paddingBottom: ".5em" }}>
<Forms.FormText style={{ paddingLeft: ".5em", paddingRight: ".5em" }}>{selector}</Forms.FormText>
<div style={{ paddingLeft: "1.5em", paddingRight: "1.5em" }}>
{properties.map(([prop, value], propIndex) => (
<Forms.FormText key={propIndex}>
<strong>{prop}:</strong>
<TextInput
value={overrides[selector]?.[prop] ?? ""}
placeholder={value}
onChange={v => {
setOverrides(prev => ({
...prev,
[selector]: {
...(prev[selector] || {}),
[prop]: v
}
}));
}}
onBlur={e => updateOverrides(
selector,
prop,
e.currentTarget.value
)}
style={{ marginBottom: ".5em" }}
/>
</Forms.FormText>
))}
</div>
</Card>
);
})}
</Forms.FormSection>
);
}

function Validator({ link, rawLink }: { link: string; rawLink: string; }) {
const [res, err, pending] = useAwaiter(() => fetchOnlineThemes(link));
const [showOverride, setShowOverride] = useState(false);

const text = pending
? "Checking..."
: err
? `Error: ${err instanceof Error ? err.message : String(err)}`
: "Valid!";

return <Forms.FormText style={{
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
}}>{text}</Forms.FormText>;
return (
<>
<Forms.FormText style={{
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
}}>{text}</Forms.FormText>
{!err && !pending && res && (
<>
<Button className={Margins.top8} onClick={() => setShowOverride(!showOverride)}>
{showOverride ? "Hide" : "Show"} CSS Override
</Button>
{showOverride && <OnlineThemeOverride rawCssText={res} rawLink={rawLink} />}
</>
)}
</>
);
}

function Validators({ themeLinks }: { themeLinks: string[]; }) {
Expand Down Expand Up @@ -96,7 +244,7 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
}}>
{label}
</Forms.FormTitle>
<Validator link={link} />
<Validator link={link} rawLink={rawLink} />
</Card>;
})}
</div>
Expand Down Expand Up @@ -296,6 +444,9 @@ function ThemesTab() {
.map(s => s.trim())
.filter(Boolean)
)];
settings.onlineThemeOverrides = Object.fromEntries(
Object.entries(settings.onlineThemeOverrides).filter(([key]) => settings.themeLinks.includes(key))
);
}

function renderOnlineThemes() {
Expand Down
6 changes: 5 additions & 1 deletion src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,10 +575,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "RamziAH",
id: 1279957227612147747n,
},
SomeAspy: {
SomeAspy: {
name: "SomeAspy",
id: 516750892372852754n,
},
yuna0x0: {
name: "yuna0x0",
id: 213656926414831616n,
},
} satisfies Record<string, Dev>);

// iife so #__PURE__ works correctly
Expand Down
6 changes: 4 additions & 2 deletions src/utils/quickCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function toggle(isEnabled: boolean) {
async function initThemes() {
themesStyle ??= createStyle("vencord-themes");

const { themeLinks, enabledThemes } = Settings;
const { themeLinks, enabledThemes, onlineThemeOverrides } = Settings;

// "darker" and "midnight" both count as dark
const activeTheme = ThemeStore.theme === "light" ? "light" : "dark";
Expand All @@ -85,7 +85,8 @@ async function initThemes() {
links.push(...localThemes);
}

themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n");
themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n") +
"\n" + Object.values(onlineThemeOverrides).join("\n");
}

document.addEventListener("DOMContentLoaded", () => {
Expand All @@ -97,6 +98,7 @@ document.addEventListener("DOMContentLoaded", () => {

SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
SettingsStore.addChangeListener("onlineThemeOverrides", initThemes);
ThemeStore.addChangeListener(initThemes);

if (!IS_WEB)
Expand Down

0 comments on commit b59f39e

Please sign in to comment.