/* * Vencord, a Discord client mod * Copyright (c) 2024 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import "./styles.css"; import * as DataStore from "@api/DataStore"; import { Settings } from "@api/Settings"; import { ErrorCard } from "@components/ErrorCard"; import { HeadingPrimary, HeadingTertiary } from "@components/Heading"; import { OpenExternalIcon } from "@components/Icons"; import { SettingsTab, wrapTab } from "@components/settings"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { Button, Forms, React, SearchableSelect, TabBar, TextInput, useEffect, useState } from "@webpack/common"; import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types"; import { ThemeCard } from "./ThemeCard"; const InputStyles = findByPropsLazy("inputWrapper", "inputError", "error"); export const apiUrl = "https://discord-themes.com/api"; export 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 = `${apiUrl}/${theme.id}`; } }); 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(apiUrl + path, { ...options, headers: { ...options.headers, } }); } 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 onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query })); const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const themeFilter = (theme: Theme) => { const enabled = themeLinks.includes(`${apiUrl}/${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 token = await DataStore.get("ThemeLibrary_uniqueToken"); const response = await themeRequest("/likes/get", { headers: { "Authorization": `Bearer ${token}`, }, }); 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); }, []); 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 ? (

Getting the latest themes...

This won't take long!

) : ( <> {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) => ( ))}
Themes
onStatusChange(v as SearchStatus)} closeOnSelect={true} />
{filteredThemes.length ? filteredThemes.map((theme: Theme) => ( )) :

No theme found.

Try narrowing your search down.

}
)}
); } // rework this! function SubmitThemes() { return (

This tab was replaced in favour of the new website:

discord-themes.com

Thank you for your understanding!

); } function ThemeLibrary() { const [currentTab, setCurrentTab] = useState(TabItem.THEMES); return ( Themes Submit Theme {currentTab === TabItem.THEMES ? : } ); } export default wrapTab(ThemeLibrary, "Theme Library");