feat: initial usercss support
Parses UserCSS/UserStyle files (.user.css) but doesn't do anything special yet with the variables. This is a first step towards supporting UserCSS themes.
This commit is contained in:
parent
c165725297
commit
2ef2baafbe
10 changed files with 318 additions and 100 deletions
|
@ -37,6 +37,7 @@
|
||||||
"eslint-plugin-simple-header": "^1.0.2",
|
"eslint-plugin-simple-header": "^1.0.2",
|
||||||
"fflate": "^0.7.4",
|
"fflate": "^0.7.4",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
|
"usercss-meta": "^0.12.0",
|
||||||
"virtual-merge": "^1.0.1"
|
"virtual-merge": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -27,6 +27,9 @@ dependencies:
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
|
usercss-meta:
|
||||||
|
specifier: ^0.12.0
|
||||||
|
version: 0.12.0
|
||||||
virtual-merge:
|
virtual-merge:
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
@ -3246,6 +3249,11 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/usercss-meta@0.12.0:
|
||||||
|
resolution: {integrity: sha512-zKrXCKdpeIwtVe87omxGo9URf+7mbozduMZEg79dmT4KB3XJwfIkEi/Uk0PcTwR/nZLtAK1+k7isgbGB/g6E7Q==}
|
||||||
|
engines: {node: '>=8.3'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
import { IpcEvents } from "@utils/IpcEvents";
|
import { IpcEvents } from "@utils/IpcEvents";
|
||||||
import { IpcRes } from "@utils/types";
|
import { IpcRes } from "@utils/types";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import type { UserThemeHeader } from "main/themes";
|
|
||||||
|
import type { ThemeHeader } from "./main/themes";
|
||||||
|
|
||||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||||
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
||||||
|
@ -22,7 +23,7 @@ export default {
|
||||||
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
||||||
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
|
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
|
||||||
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
|
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
|
||||||
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
|
getThemesList: () => invoke<Array<ThemeHeader>>(IpcEvents.GET_THEMES_LIST),
|
||||||
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName)
|
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,11 @@ import { showItemInFolder } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
|
||||||
import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||||
import { UserThemeHeader } from "main/themes";
|
import { UserThemeHeader } from "main/themes/bd";
|
||||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
import { UserstyleHeader } from "usercss-meta";
|
||||||
|
|
||||||
|
import type { ThemeHeader } from "../../main/themes";
|
||||||
import { AddonCard } from "./AddonCard";
|
import { AddonCard } from "./AddonCard";
|
||||||
import { SettingsTab, wrapTab } from "./shared";
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
|
@ -41,6 +43,7 @@ type FileInput = ComponentType<{
|
||||||
|
|
||||||
const InviteActions = findByPropsLazy("resolveInvite");
|
const InviteActions = findByPropsLazy("resolveInvite");
|
||||||
const TrashIcon = findByCodeLazy("M5 6.99902V18.999C5 20.101 5.897 20.999");
|
const TrashIcon = findByCodeLazy("M5 6.99902V18.999C5 20.101 5.897 20.999");
|
||||||
|
const CogWheel = findByCodeLazy("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069");
|
||||||
const FileInput: FileInput = findByCodeLazy("activateUploadDialogue=");
|
const FileInput: FileInput = findByCodeLazy("activateUploadDialogue=");
|
||||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
|
@ -94,14 +97,52 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThemeCardProps {
|
interface BDThemeCardProps {
|
||||||
theme: UserThemeHeader;
|
theme: UserThemeHeader;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onChange: (enabled: boolean) => void;
|
onChange: (enabled: boolean) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
|
interface UserCSSCardProps {
|
||||||
|
theme: UserstyleHeader;
|
||||||
|
enabled: boolean;
|
||||||
|
onChange: (enabled: boolean) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardProps) {
|
||||||
|
return (
|
||||||
|
<AddonCard
|
||||||
|
name={theme.name}
|
||||||
|
description={theme.description}
|
||||||
|
author={theme.author ?? "Unknown"}
|
||||||
|
enabled={enabled}
|
||||||
|
setEnabled={onChange}
|
||||||
|
infoButton={
|
||||||
|
<>
|
||||||
|
<div style={{ cursor: "pointer" }}>
|
||||||
|
<CogWheel />
|
||||||
|
</div>
|
||||||
|
{IS_WEB && (
|
||||||
|
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
|
||||||
|
<TrashIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
|
||||||
|
{!!theme.homepageURL && <Link href={theme.homepageURL}>Homepage</Link>}
|
||||||
|
{!!(theme.homepageURL && theme.supportURL) && " • "}
|
||||||
|
{!!theme.supportURL && <Link href={theme.supportURL}>Support</Link>}
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BDThemeCard({ theme, enabled, onChange, onDelete }: BDThemeCardProps) {
|
||||||
return (
|
return (
|
||||||
<AddonCard
|
<AddonCard
|
||||||
name={theme.name}
|
name={theme.name}
|
||||||
|
@ -156,7 +197,7 @@ function ThemesTab() {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
||||||
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
||||||
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
|
const [userThemes, setUserThemes] = useState<ThemeHeader[] | null>(null);
|
||||||
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
|
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -259,19 +300,32 @@ function ThemesTab() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
{userThemes?.map(theme => (
|
{userThemes?.map(({ type, header: theme }: ThemeHeader) => (
|
||||||
<ThemeCard
|
type === "bd" ? (
|
||||||
key={theme.fileName}
|
<BDThemeCard
|
||||||
enabled={settings.enabledThemes.includes(theme.fileName)}
|
key={theme.fileName}
|
||||||
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
enabled={settings.enabledThemes.includes(theme.fileName)}
|
||||||
onDelete={async () => {
|
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
||||||
onLocalThemeChange(theme.fileName, false);
|
onDelete={async () => {
|
||||||
await VencordNative.themes.deleteTheme(theme.fileName);
|
onLocalThemeChange(theme.fileName, false);
|
||||||
refreshLocalThemes();
|
await VencordNative.themes.deleteTheme(theme.fileName);
|
||||||
}}
|
refreshLocalThemes();
|
||||||
theme={theme}
|
}}
|
||||||
/>
|
theme={theme as UserThemeHeader}
|
||||||
))}
|
/>
|
||||||
|
) : (
|
||||||
|
<UserCSSThemeCard
|
||||||
|
key={theme.fileName}
|
||||||
|
enabled={settings.enabledThemes.includes(theme.fileName)}
|
||||||
|
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
||||||
|
onDelete={async () => {
|
||||||
|
onLocalThemeChange(theme.fileName, false);
|
||||||
|
await VencordNative.themes.deleteTheme(theme.fileName);
|
||||||
|
refreshLocalThemes();
|
||||||
|
}}
|
||||||
|
theme={theme as UserstyleHeader}
|
||||||
|
/>
|
||||||
|
)))}
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -29,7 +29,9 @@ import { join, normalize } from "path";
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
||||||
|
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
import type { ThemeHeader } from "./themes";
|
||||||
|
import { getThemeInfo, stripBOM } from "./themes/bd";
|
||||||
|
import { parse as usercssParse } from "./themes/usercss";
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
||||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
|
|
||||||
|
@ -47,10 +49,10 @@ function readCss() {
|
||||||
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
|
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listThemes(): Promise<UserThemeHeader[]> {
|
async function listThemes(): Promise<ThemeHeader[]> {
|
||||||
const files = await readdir(THEMES_DIR).catch(() => []);
|
const files = await readdir(THEMES_DIR).catch(() => []);
|
||||||
|
|
||||||
const themeInfo: UserThemeHeader[] = [];
|
const themeInfo: ThemeHeader[] = [];
|
||||||
|
|
||||||
for (const fileName of files) {
|
for (const fileName of files) {
|
||||||
if (!fileName.endsWith(".css")) continue;
|
if (!fileName.endsWith(".css")) continue;
|
||||||
|
@ -58,7 +60,19 @@ async function listThemes(): Promise<UserThemeHeader[]> {
|
||||||
const data = await getThemeData(fileName).then(stripBOM).catch(() => null);
|
const data = await getThemeData(fileName).then(stripBOM).catch(() => null);
|
||||||
if (data == null) continue;
|
if (data == null) continue;
|
||||||
|
|
||||||
themeInfo.push(getThemeInfo(data, fileName));
|
if (fileName.endsWith(".user.css")) {
|
||||||
|
// handle it as usercss
|
||||||
|
themeInfo.push({
|
||||||
|
type: "usercss",
|
||||||
|
header: usercssParse(data, fileName)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// presumably BD but could also be plain css
|
||||||
|
themeInfo.push({
|
||||||
|
type: "bd",
|
||||||
|
header: getThemeInfo(data, fileName)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return themeInfo;
|
return themeInfo;
|
||||||
|
|
81
src/main/themes/bd/index.ts
Normal file
81
src/main/themes/bd/index.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/* eslint-disable simple-header/header */
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* BetterDiscord addon meta parser
|
||||||
|
* Copyright 2023 BetterDiscord contributors
|
||||||
|
* Copyright 2023 Vendicated and Vencord contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
|
||||||
|
const escapedAtRegex = /^\\@/;
|
||||||
|
|
||||||
|
export interface UserThemeHeader {
|
||||||
|
fileName: string;
|
||||||
|
name: string;
|
||||||
|
author: string;
|
||||||
|
description: string;
|
||||||
|
version?: string;
|
||||||
|
license?: string;
|
||||||
|
source?: string;
|
||||||
|
website?: string;
|
||||||
|
invite?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeHeader(fileName: string, opts: Partial<UserThemeHeader> = {}): UserThemeHeader {
|
||||||
|
return {
|
||||||
|
fileName,
|
||||||
|
name: opts.name ?? fileName.replace(/\.css$/i, ""),
|
||||||
|
author: opts.author ?? "Unknown Author",
|
||||||
|
description: opts.description ?? "A Discord Theme.",
|
||||||
|
version: opts.version,
|
||||||
|
license: opts.license,
|
||||||
|
source: opts.source,
|
||||||
|
website: opts.website,
|
||||||
|
invite: opts.invite
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripBOM(fileContent: string) {
|
||||||
|
if (fileContent.charCodeAt(0) === 0xFEFF) {
|
||||||
|
fileContent = fileContent.slice(1);
|
||||||
|
}
|
||||||
|
return fileContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeInfo(css: string, fileName: string): UserThemeHeader {
|
||||||
|
if (!css) return makeHeader(fileName);
|
||||||
|
|
||||||
|
const block = css.split("/**", 2)?.[1]?.split("*/", 1)?.[0];
|
||||||
|
if (!block) return makeHeader(fileName);
|
||||||
|
|
||||||
|
const header: Partial<UserThemeHeader> = {};
|
||||||
|
let field = "";
|
||||||
|
let accum = "";
|
||||||
|
for (const line of block.split(splitRegex)) {
|
||||||
|
if (line.length === 0) continue;
|
||||||
|
if (line.charAt(0) === "@" && line.charAt(1) !== " ") {
|
||||||
|
header[field] = accum.trim();
|
||||||
|
const l = line.indexOf(" ");
|
||||||
|
field = line.substring(1, l);
|
||||||
|
accum = line.substring(l + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
accum += " " + line.replace("\\n", "\n").replace(escapedAtRegex, "@");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header[field] = accum.trim();
|
||||||
|
delete header[""];
|
||||||
|
return makeHeader(fileName, header);
|
||||||
|
}
|
|
@ -1,81 +1,17 @@
|
||||||
/* eslint-disable simple-header/header */
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
/*!
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
* BetterDiscord addon meta parser
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
* Copyright 2023 BetterDiscord contributors
|
|
||||||
* Copyright 2023 Vendicated and Vencord contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
|
import type { UserstyleHeader } from "usercss-meta";
|
||||||
const escapedAtRegex = /^\\@/;
|
|
||||||
|
|
||||||
export interface UserThemeHeader {
|
import type { UserThemeHeader } from "./bd";
|
||||||
fileName: string;
|
|
||||||
name: string;
|
|
||||||
author: string;
|
|
||||||
description: string;
|
|
||||||
version?: string;
|
|
||||||
license?: string;
|
|
||||||
source?: string;
|
|
||||||
website?: string;
|
|
||||||
invite?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeHeader(fileName: string, opts: Partial<UserThemeHeader> = {}): UserThemeHeader {
|
export type ThemeHeader = {
|
||||||
return {
|
type: "bd";
|
||||||
fileName,
|
header: UserThemeHeader;
|
||||||
name: opts.name ?? fileName.replace(/\.css$/i, ""),
|
} | {
|
||||||
author: opts.author ?? "Unknown Author",
|
type: "usercss";
|
||||||
description: opts.description ?? "A Discord Theme.",
|
header: UserstyleHeader;
|
||||||
version: opts.version,
|
};
|
||||||
license: opts.license,
|
|
||||||
source: opts.source,
|
|
||||||
website: opts.website,
|
|
||||||
invite: opts.invite
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripBOM(fileContent: string) {
|
|
||||||
if (fileContent.charCodeAt(0) === 0xFEFF) {
|
|
||||||
fileContent = fileContent.slice(1);
|
|
||||||
}
|
|
||||||
return fileContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getThemeInfo(css: string, fileName: string): UserThemeHeader {
|
|
||||||
if (!css) return makeHeader(fileName);
|
|
||||||
|
|
||||||
const block = css.split("/**", 2)?.[1]?.split("*/", 1)?.[0];
|
|
||||||
if (!block) return makeHeader(fileName);
|
|
||||||
|
|
||||||
const header: Partial<UserThemeHeader> = {};
|
|
||||||
let field = "";
|
|
||||||
let accum = "";
|
|
||||||
for (const line of block.split(splitRegex)) {
|
|
||||||
if (line.length === 0) continue;
|
|
||||||
if (line.charAt(0) === "@" && line.charAt(1) !== " ") {
|
|
||||||
header[field] = accum.trim();
|
|
||||||
const l = line.indexOf(" ");
|
|
||||||
field = line.substring(1, l);
|
|
||||||
accum = line.substring(l + 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
accum += " " + line.replace("\\n", "\n").replace(escapedAtRegex, "@");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header[field] = accum.trim();
|
|
||||||
delete header[""];
|
|
||||||
return makeHeader(fileName, header);
|
|
||||||
}
|
|
||||||
|
|
15
src/main/themes/usercss/index.ts
Normal file
15
src/main/themes/usercss/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parse as originalParse, UserstyleHeader } from "usercss-meta";
|
||||||
|
|
||||||
|
export function parse(text: string, fileName: string): UserstyleHeader {
|
||||||
|
const { metadata } = originalParse(text.replace(/\r/g, ""));
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
fileName,
|
||||||
|
};
|
||||||
|
}
|
108
src/main/themes/usercss/usercss-meta.d.ts
vendored
Normal file
108
src/main/themes/usercss/usercss-meta.d.ts
vendored
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module "usercss-meta" {
|
||||||
|
import { Simplify } from "type-fest";
|
||||||
|
|
||||||
|
export type UserCSSVariable = Simplify<{ name: string; label: string; } & (
|
||||||
|
| {
|
||||||
|
type: "text";
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "color";
|
||||||
|
// Hex, rgb(), rgba()
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "checkbox";
|
||||||
|
default: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "range";
|
||||||
|
default: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
units?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "number";
|
||||||
|
default: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "select";
|
||||||
|
default: string;
|
||||||
|
options: Record<string, string>;
|
||||||
|
}
|
||||||
|
)>;
|
||||||
|
|
||||||
|
export interface UserstyleHeader {
|
||||||
|
/**
|
||||||
|
* The file name of the UserCSS style.
|
||||||
|
*
|
||||||
|
* @vencord Specific to Vencord, not part of the original module.
|
||||||
|
*/
|
||||||
|
fileName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of your style.
|
||||||
|
*
|
||||||
|
* The combination of {@link name} and {@link namespace} must be unique.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The namespace of the style. Helps to distinguish between styles with the same name.
|
||||||
|
*
|
||||||
|
* The combination of {@link name} and {@link namespace} must be unique.
|
||||||
|
*/
|
||||||
|
namespace: string;
|
||||||
|
/**
|
||||||
|
* The version of your style.
|
||||||
|
*/
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A short significant description.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* The author of the style.
|
||||||
|
*/
|
||||||
|
author?: string;
|
||||||
|
/**
|
||||||
|
* The project's homepage.
|
||||||
|
*
|
||||||
|
* This is not an update URL. See {@link updateURL}.
|
||||||
|
*/
|
||||||
|
homepageURL?: string;
|
||||||
|
/**
|
||||||
|
* The URL the user can report issues to the style author.
|
||||||
|
*/
|
||||||
|
supportURL?: string;
|
||||||
|
/**
|
||||||
|
* The URL used when updating the style.
|
||||||
|
*/
|
||||||
|
updateURL?: string;
|
||||||
|
/**
|
||||||
|
* The SPDX license identifier for this style. If none is included, the style is assumed to be All Rights Reserved.
|
||||||
|
*/
|
||||||
|
license?: string;
|
||||||
|
/**
|
||||||
|
* The CSS preprocessor used to write this style.
|
||||||
|
*
|
||||||
|
* @vencord Unimplemented in Vencord, just part of the metadata.
|
||||||
|
*/
|
||||||
|
preprocessor?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of variables the style defines.
|
||||||
|
*/
|
||||||
|
vars: Record<string, UserCSSVariable>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse(text: string): { metadata: UserstyleHeader; };
|
||||||
|
}
|
Loading…
Reference in a new issue