/* * Vencord, a Discord client mod * Copyright (c) 2024 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ /* eslint-disable indent */ import "./styles.css"; import { generateId } from "@api/Commands"; import * as DataStore from "@api/DataStore"; import { Settings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { CodeBlock } from "@components/CodeBlock"; import { ErrorCard } from "@components/ErrorCard"; import { OpenExternalIcon } from "@components/Icons"; import { SettingsTab, wrapTab } from "@components/VencordSettings/shared"; import { proxyLazy } from "@utils/lazy"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { openModal } from "@utils/modal"; import { findByPropsLazy, findLazy } from "@webpack"; import { Button, Card, FluxDispatcher, Forms, React, SearchableSelect, TabBar, TextArea, TextInput, Toasts, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; import { User } from "discord-types/general"; import { Constructor } from "type-fest"; import { isAuthorized } from "../auth"; import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types"; import { LikesComponent } from "./LikesComponent"; import { ThemeInfoModal } from "./ThemeInfoModal"; const cl = classNameFactory("vc-plugins-"); const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error"); const UserRecord: Constructor> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any; const TextAreaProps = findLazy(m => typeof m.textarea === "string"); const API_URL = "https://themes-delta.vercel.app/api"; const logger = new Logger("ThemeLibrary", "#e5c890"); export async function fetchAllThemes(): Promise { const response = await themeRequest("/themes"); const data = await response.json(); const themes: Theme[] = Object.values(data); themes.forEach(theme => { if (!theme.source) { theme.source = `${API_URL}/${theme.name}`; } }); return themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime()); } export async function themeRequest(path: string, options: RequestInit = {}) { return fetch(API_URL + path, { ...options, headers: { ...options.headers, } }); } function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) { const newUser = new UserRecord({ username: user.username, id: user.id ?? generateId(), avatar: user.avatar, bot: true, }); FluxDispatcher.dispatch({ type: "USER_UPDATE", user: newUser, }); return newUser; } const themeTemplate = `/** * @name [Theme name] * @author [Your name] * @description [Your Theme Description] * @version [Your Theme Version] * @donate [Optionally, your Donation Link] * @tags [Optionally, tags that apply to your theme] * @invite [Optionally, your Support Server Invite] * @source [Optionally, your source code link] */ /* Your CSS goes here */ `; function ThemeTab() { const [themes, setThemes] = useState([]); const [filteredThemes, setFilteredThemes] = useState([]); const [themeLinks, setThemeLinks] = useState(Vencord.Settings.themeLinks); const [likedThemes, setLikedThemes] = useState(); const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL }); const [hideWarningCard, setHideWarningCard] = useState(Settings.plugins.ThemeLibrary.hideWarningCard); const [loading, setLoading] = useState(true); const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id }); const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query })); const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const themeFilter = (theme: Theme) => { const enabled = themeLinks.includes(`${API_URL}/${theme.name}`); if (enabled && searchValue.status === SearchStatus.DISABLED) return false; if (!theme.tags.includes("theme") && searchValue.status === SearchStatus.THEME) return false; if (!theme.tags.includes("snippet") && searchValue.status === SearchStatus.SNIPPET) return false; if (!theme.tags.includes("dark") && searchValue.status === SearchStatus.DARK) return false; if (!theme.tags.includes("light") && searchValue.status === SearchStatus.LIGHT) return false; if (!enabled && searchValue.status === SearchStatus.ENABLED) return false; if (!searchValue.value.length) return true; const v = searchValue.value.toLowerCase(); return ( theme.name.toLowerCase().includes(v) || theme.description.toLowerCase().includes(v) || theme.author.discord_name.toLowerCase().includes(v) || theme.tags?.some(t => t.toLowerCase().includes(v)) ); }; const fetchLikes = async () => { try { const response = await themeRequest("/likes/get"); const data = await response.json(); return data; } catch (err) { logger.error(err); } }; useEffect(() => { const fetchThemes = async () => { try { const themes = await fetchAllThemes(); // fetch likes setThemes(themes); const likes = await fetchLikes(); setLikedThemes(likes); setFilteredThemes(themes); } catch (err) { logger.error(err); } finally { setLoading(false); } }; fetchThemes(); }, []); useEffect(() => { setThemeLinks(Vencord.Settings.themeLinks); }, [Vencord.Settings.themeLinks]); useEffect(() => { const filteredThemes = themes.filter(themeFilter); setFilteredThemes(filteredThemes); }, [searchValue]); return (
<> {loading ? (
Loading themes...
) : ( <> {hideWarningCard ? null : ( Want your theme removed? If you want your theme(s) permanently removed, please open an issue on GitHub )}
Newest Additions {themes.slice(0, 2).map((theme: Theme) => ( {theme.name} {theme.description}
{theme.tags && ( {theme.tags.map(tag => ( {tag} ))} )}
{themeLinks.includes(`${API_URL}/${theme.name}`) ? ( ) : ( )}
))}
Themes
onStatusChange(v as SearchStatus)} closeOnSelect={true} className={InputStyles.inputDefault} />
{filteredThemes.map((theme: Theme) => ( {theme.name} {theme.description} {theme.name}
{theme.tags && ( {theme.tags.map(tag => ( {tag} ))} )}
{themeLinks.includes(`${API_URL}/${theme.name}`) ? ( ) : ( )}
))}
)}
); } function SubmitThemes() { const [themeContent, setContent] = useState(""); const handleChange = (v: string) => setContent(v); return (
Theme Guidelines Follow the formatting for your CSS to get credit for your theme. You can find the template below. (your theme will be reviewed and can take up to 24 hours to be approved) Submit Themes If you plan on updating your theme / snippet frequently, consider using an @import instead!