/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2022 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import { Settings, useSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { ErrorCard } from "@components/ErrorCard"; import { Flex } from "@components/Flex"; import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { Link } from "@components/Link"; import { openPluginModal } from "@components/PluginSettings/PluginModal"; import type { UserThemeHeader } from "@main/themes"; import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { relaunch } from "@utils/native"; import { useAwaiter, useForceUpdater } from "@utils/react"; import { getStylusWebStoreUrl } from "@utils/web"; import { findLazy } from "@webpack"; import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import type { ComponentType, Ref, SyntheticEvent } from "react"; import Plugins from "~plugins"; import { AddonCard } from "./AddonCard"; import { QuickAction, QuickActionCard } from "./quickActions"; import { SettingsTab, wrapTab } from "./shared"; type FileInput = ComponentType<{ ref: Ref; onChange: (e: SyntheticEvent) => void; multiple?: boolean; filters?: { name?: string; extensions: string[]; }[]; }>; const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef); const cl = classNameFactory("vc-settings-theme-"); function Validator({ link }: { link: string; }) { const [res, err, pending] = useAwaiter(() => 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!"; })); const text = pending ? "Checking..." : err ? `Error: ${err instanceof Error ? err.message : String(err)}` : "Valid!"; return {text}; } function Validators({ themeLinks }: { themeLinks: string[]; }) { if (!themeLinks.length) return null; return ( <> Validator This section will tell you whether your themes can successfully be loaded
{themeLinks.map(rawLink => { const { label, link } = (() => { const match = /^@(light|dark) (.*)/.exec(rawLink); if (!match) return { label: rawLink, link: rawLink }; const [, mode, link] = match; return { label: `[${mode} mode only] ${link}`, link }; })(); return {label} ; })}
); } interface ThemeCardProps { theme: UserThemeHeader; enabled: boolean; onChange: (enabled: boolean) => void; onDelete: () => void; } function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) { return ( ) } footer={ {!!theme.website && Website} {!!(theme.website && theme.invite) && " • "} {!!theme.invite && ( { e.preventDefault(); theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite")); }} > Discord Server )} } /> ); } enum ThemeTab { LOCAL, ONLINE } function ThemesTab() { const settings = useSettings(["themeLinks", "enabledThemes"]); const fileInputRef = useRef(null); const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); const [themeText, setThemeText] = useState(settings.themeLinks.join("\n")); const [userThemes, setUserThemes] = useState(null); const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir); useEffect(() => { refreshLocalThemes(); }, []); async function refreshLocalThemes() { const themes = await VencordNative.themes.getThemesList(); setUserThemes(themes); } // When a local theme is enabled/disabled, update the settings function onLocalThemeChange(fileName: string, value: boolean) { if (value) { if (settings.enabledThemes.includes(fileName)) return; settings.enabledThemes = [...settings.enabledThemes, fileName]; } else { settings.enabledThemes = settings.enabledThemes.filter(f => f !== fileName); } } async function onFileUpload(e: SyntheticEvent) { e.stopPropagation(); e.preventDefault(); if (!e.currentTarget?.files?.length) return; const { files } = e.currentTarget; const uploads = Array.from(files, file => { const { name } = file; if (!name.endsWith(".css")) return; return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { VencordNative.themes.uploadTheme(name, reader.result as string) .then(resolve) .catch(reject); }; reader.readAsText(file); }); }); await Promise.all(uploads); refreshLocalThemes(); } function renderLocalThemes() { return ( <> Find Themes:
BetterDiscord Themes GitHub
If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.
External Resources For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked. Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts. <> {IS_WEB ? ( Upload Theme } Icon={PlusIcon} /> ) : ( VencordNative.themes.openFolder()} disabled={themeDirPending} Icon={FolderIcon} /> )} VencordNative.quickCss.openEditor()} Icon={PaintbrushIcon} /> {Settings.plugins.ClientTheme.enabled && ( openPluginModal(Plugins.ClientTheme)} Icon={PencilIcon} /> )}
{userThemes?.map(theme => ( onLocalThemeChange(theme.fileName, enabled)} onDelete={async () => { onLocalThemeChange(theme.fileName, false); await VencordNative.themes.deleteTheme(theme.fileName); refreshLocalThemes(); }} theme={theme} /> ))}
); } // When the user leaves the online theme textbox, update the settings function onBlur() { settings.themeLinks = [...new Set( themeText .trim() .split(/\n+/) .map(s => s.trim()) .filter(Boolean) )]; } function renderOnlineThemes() { return ( <> Paste links to css files here One link per line You can prefix lines with @light or @dark to toggle them based on your Discord theme Make sure to use direct links to files (raw or github.io)!