/*
* 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 (
<>
ValidatorThis section will tell you whether your themes can successfully be loaded
If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.External ResourcesFor 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}
/>
)}
>
>
);
}
// 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 hereOne link per lineYou can prefix lines with @light or @dark to toggle them based on your Discord themeMake sure to use direct links to files (raw or github.io)!
>
);
}
return (
Local Themes
Online Themes
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
);
}
export function CspErrorCard() {
if (IS_WEB) return null;
const errors = useCspErrors();
const forceUpdate = useForceUpdater();
if (!errors.length) return null;
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
const allowUrl = async (url: string) => {
const { origin: baseUrl, host } = new URL(url);
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
if (result !== "ok") return;
CspBlockedUrls.forEach(url => {
if (new URL(url).host === host) {
CspBlockedUrls.delete(url);
}
});
forceUpdate();
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
};
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
return (
Blocked ResourcesSome images, styles, or fonts were blocked because they come from disallowed domains.It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
Blocked URLs
{errors.map((url, i) => (
{i !== 0 && }
{url}
))}
{hasImgurHtmlDomain && (
<>
Imgur links should be direct links in the form of https://i.imgur.com/...To obtain a direct link, right-click the image and select "Copy image address".
>
)}
);
}
function UserscriptThemesTab() {
return (
Themes are not supported on the Userscript!
You can instead install themes with the Stylus extension!
);
}
export default IS_USERSCRIPT
? wrapTab(UserscriptThemesTab, "Themes")
: wrapTab(ThemesTab, "Themes");