/* * Vencord, a Discord client mod * Copyright (c) 2024 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import "./styles.css"; import { generateId } from "@api/Commands"; import * as DataStore from "@api/DataStore"; import { Settings } from "@api/Settings"; 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 { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByPropsLazy } from "@webpack"; import { Button, Card, FluxDispatcher, Forms, Parser, 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 { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types"; import { isAuthorized } from "../utils/auth"; import { LikesComponent } from "./LikesComponent"; import { ThemeInfoModal } from "./ThemeInfoModal"; const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error"); const UserRecord: Constructor> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any; 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 */ `; const SearchTags = { [SearchStatus.THEME]: "THEME", [SearchStatus.SNIPPET]: "SNIPPET", [SearchStatus.LIKED]: "LIKED", [SearchStatus.DARK]: "DARK", [SearchStatus.LIGHT]: "LIGHT", }; 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}`); const tags = new Set(theme.tags.map(tag => tag?.toLowerCase())); if (!enabled && searchValue.status === SearchStatus.ENABLED) return false; const anyTags = SearchTags[searchValue.status]; if (anyTags && !tags.has(anyTags?.toLowerCase())) return false; if ((enabled && searchValue.status === SearchStatus.DISABLED) || (!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) || (Array.isArray(theme.author) ? theme.author.some(author => author.discord_name?.toLowerCase()?.includes(v)) : theme.author.discord_name?.toLowerCase()?.includes(v)) || tags.has(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 fetchData = async () => { try { const [themes, likes] = await Promise.all([fetchAllThemes(), fetchLikes()]); setThemes(themes); setLikedThemes(likes); setFilteredThemes(themes); } catch (err) { logger.error(err); } finally { setLoading(false); } }; fetchData(); }, []); useEffect(() => { setThemeLinks(Vencord.Settings.themeLinks); }, [Vencord.Settings.themeLinks]); useEffect(() => { // likes only update after 12_000 due to cache if (searchValue.status === SearchStatus.LIKED) { const likedThemes = themes.sort((a, b) => b.likes - a.likes); // replacement of themeFilter which wont work with SearchStatus.LIKED const filteredLikedThemes = likedThemes.filter(x => x.name.includes(searchValue.value)); setFilteredThemes(filteredLikedThemes); } else { const sortedThemes = themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime()); const filteredThemes = sortedThemes.filter(themeFilter); setFilteredThemes(filteredThemes); } }, [searchValue, themes]); return (
<> {loading ? (
Loading themes...
) : ( <> {hideWarningCard ? null : ( Want your theme removed? If you want your theme(s) permanently removed, please open an issue on GitHub )}
{searchValue.status === SearchStatus.LIKED ? "Most Liked" : "Newest Additions"} {themes.slice(0, 2).map((theme: Theme) => ( {theme.name} {Parser.parse(theme.description)}
{theme.tags && ( {theme.tags.map(tag => ( {tag} ))} )}
{themeLinks.includes(`${API_URL}/${theme.name}`) ? ( ) : ( )); } else { setThemeLinks(onlineThemeLinks); Vencord.Settings.themeLinks = onlineThemeLinks; } }} size={Button.Sizes.MEDIUM} color={Button.Colors.GREEN} look={Button.Looks.FILLED} className={Margins.right8} > Add Theme )}
))}
Themes
onStatusChange(v as SearchStatus)} closeOnSelect={true} className={InputStyles.inputDefault} />
{filteredThemes.map((theme: Theme) => ( {theme.name} {Parser.parse(theme.description)} {theme.name}
{theme.tags && ( {theme.tags.map(tag => ( {tag} ))} )}
{themeLinks.includes(`${API_URL}/${theme.name}`) ? ( ) : ( )); } else { setThemeLinks(onlineThemeLinks); Vencord.Settings.themeLinks = onlineThemeLinks; } }} size={Button.Sizes.MEDIUM} color={Button.Colors.GREEN} look={Button.Looks.FILLED} className={Margins.right8} > Add Theme )}
))}
)}
); } 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!