/* * 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 { 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, Select, showToast, 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 { LikesComponent } from "./LikesComponent"; import { ThemeInfoModal } from "./ThemeInfoModal"; const cl = classNameFactory("vc-plugins-"); const InputStyles = findByPropsLazy("inputDefault", "inputWrapper"); 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"); async function fetchThemes(url: string): Promise { const response = await fetch(url); const data = await response.json(); const themes: Theme[] = Object.values(data); themes.forEach(theme => { if (!theme.source) { theme.source = `${API_URL}/${theme.name}`; } else { theme.source = theme.source.replace("?raw=true", "") + "?raw=true"; } }); return themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime()); } function API_TYPE(theme: Theme | Object, returnAll?: boolean) { if (!theme) return; const settings = Settings.plugins.ThemeLibrary.domain ?? false; if (returnAll) { const url = settings ? "https://raw.githubusercontent.com/Faf4a/plugins/main/assets/meta.json" : `${API_URL}/themes`; return fetchThemes(url); } else { // @ts-ignore return settings ? theme.source : `${API_URL}/${theme.name}`; } } 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_TYPE(theme)); 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 API_TYPE({}, true); // 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_TYPE(theme)) ? ( ) : ( )}
))}
Themes