From 1e079a70e9e2a4493859b7b18c1bae3e39a75186 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:31:17 -0400 Subject: [PATCH] Update ThemeLibrary --- .../components/LikesComponent.tsx | 11 +- .../components/ThemeInfoModal.tsx | 178 +++++++++++++----- .../themeLibrary/components/ThemeTab.tsx | 66 ++++--- .../themeLibrary/components/styles.css | 26 ++- src/equicordplugins/themeLibrary/index.tsx | 2 +- src/equicordplugins/themeLibrary/native.ts | 25 +++ src/equicordplugins/themeLibrary/types.ts | 9 +- .../themeLibrary/utils/Icons.tsx | 32 ++++ .../themeLibrary/{ => utils}/auth.tsx | 2 +- .../themeLibrary/{ => utils}/settings.tsx | 0 10 files changed, 256 insertions(+), 95 deletions(-) create mode 100644 src/equicordplugins/themeLibrary/native.ts create mode 100644 src/equicordplugins/themeLibrary/utils/Icons.tsx rename src/equicordplugins/themeLibrary/{ => utils}/auth.tsx (98%) rename src/equicordplugins/themeLibrary/{ => utils}/settings.tsx (100%) diff --git a/src/equicordplugins/themeLibrary/components/LikesComponent.tsx b/src/equicordplugins/themeLibrary/components/LikesComponent.tsx index db7b0fcc..a906cf1a 100644 --- a/src/equicordplugins/themeLibrary/components/LikesComponent.tsx +++ b/src/equicordplugins/themeLibrary/components/LikesComponent.tsx @@ -9,8 +9,9 @@ import { Logger } from "@utils/Logger"; import { Button, useEffect, useRef, UserStore, useState } from "@webpack/common"; import type { User } from "discord-types/general"; -import { isAuthorized } from "../auth"; import type { Theme, ThemeLikeProps } from "../types"; +import { isAuthorized } from "../utils/auth"; +import { LikeIcon } from "../utils/Icons"; import { themeRequest } from "./ThemeTab"; export const logger = new Logger("ThemeLibrary", "#e5c890"); @@ -30,12 +31,6 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t return themeLike ? themeLike.userIds.length : 0; } - const likeIcon = (isLiked: boolean) => ( - - ); - const handleLikeClick = async (themeId: Theme["id"]) => { if (!isAuthorized()) return; const theme = likedThemes?.likes.find(like => like.themeId === themeId as unknown as Number); @@ -90,7 +85,7 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t look={Button.Looks.OUTLINED} style={{ marginLeft: "8px" }} > - {likeIcon(hasLiked)} {likesCount} + {LikeIcon(hasLiked)} {likesCount} ); diff --git a/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx b/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx index 34df22d1..c5db1157 100644 --- a/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx +++ b/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx @@ -8,21 +8,37 @@ import { CodeBlock } from "@components/CodeBlock"; import { Heart } from "@components/Heart"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import type { PluginNative } from "@utils/types"; import { findComponentByCodeLazy } from "@webpack"; import { Button, Clipboard, Forms, React, showToast, Toasts } from "@webpack/common"; -import { ThemeInfoModalProps } from "../types"; +import { Theme, ThemeInfoModalProps } from "../types"; +import { DownloadIcon } from "../utils/Icons"; +import { logger } from "./LikesComponent"; +const Native = VencordNative.pluginHelpers.ThemeLibrary as PluginNative; const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); -export const ThemeInfoModal: React.FC = ({ author, theme, ...props }) => { +async function downloadTheme(themesDir: string, theme: Theme) { + try { + await Native.downloadTheme(themesDir, theme); + showToast(`Downloaded ${theme.name}!`, Toasts.Type.SUCCESS); + } catch (err: any) { + logger.error(err); + showToast(`Failed to download ${theme.name}! (check console)`, Toasts.Type.FAILURE); + } +} +export const ThemeInfoModal: React.FC = ({ author, theme, ...props }) => { const content = window.atob(theme.content); const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || ""; const donate = metadata.match(/@donate\s+(.+)/)?.[1] || ""; const version = metadata.match(/@version\s+(.+)/)?.[1] || ""; + const { likes, guild, tags } = theme; + return ( @@ -48,6 +64,53 @@ export const ThemeInfoModal: React.FC = ({ author, theme, . {author.username} + {version && ( + <> + Version + + {version} + + + )} + Likes + + {likes === 0 ? `Nobody liked this ${theme.type} yet.` : `${likes} users liked this ${theme.type}!`} + + {donate && ( + <> + Donate + + You can support the author by donating below! + + + + + + )} + {guild && ( + <> + Support Server + + {guild.name} + + + + + + )} Source - {version && ( - <> - Version - - {version} - - - )} - {donate && ( - <> - Donate - - You can support the author by donating below. - - - - - - )} - {theme.guild && ( - <> - Support Server - - {theme.guild.name} - - - - - - )} - {theme.tags && ( + {tags && ( <> Tags - {theme.tags.map(tag => ( + {tags.map(tag => ( {tag} @@ -148,6 +168,68 @@ export const ThemeInfoModal: React.FC = ({ author, theme, . > Close + + + + + )); + } else { + await downloadTheme(themesDir, theme); + } + }} + > +
+ Download +
+ ); diff --git a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx index 023dceb0..98ea6e76 100644 --- a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx +++ b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx @@ -10,7 +10,6 @@ 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"; @@ -25,12 +24,11 @@ import { Button, Card, FluxDispatcher, Forms, React, SearchableSelect, TabBar, T import { User } from "discord-types/general"; import { Constructor } from "type-fest"; -import { isAuthorized } from "../auth"; import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types"; +import { isAuthorized } from "../utils/auth"; 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"); @@ -88,6 +86,15 @@ const themeTemplate = `/** /* 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([]); @@ -103,12 +110,15 @@ function ThemeTab() { 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; + 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(); @@ -116,7 +126,7 @@ function ThemeTab() { 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)) + tags.has(v) ); }; @@ -131,12 +141,10 @@ function ThemeTab() { }; useEffect(() => { - const fetchThemes = async () => { + const fetchData = async () => { try { - const themes = await fetchAllThemes(); - // fetch likes + const [themes, likes] = await Promise.all([fetchAllThemes(), fetchLikes()]); setThemes(themes); - const likes = await fetchLikes(); setLikedThemes(likes); setFilteredThemes(themes); } catch (err) { @@ -145,7 +153,7 @@ function ThemeTab() { setLoading(false); } }; - fetchThemes(); + fetchData(); }, []); useEffect(() => { @@ -153,9 +161,18 @@ function ThemeTab() { }, [Vencord.Settings.themeLinks]); useEffect(() => { - const filteredThemes = themes.filter(themeFilter); - setFilteredThemes(filteredThemes); - }, [searchValue]); + // 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 (
@@ -191,13 +208,13 @@ function ThemeTab() { >Hide )} -
+
- Newest Additions + {searchValue.status === SearchStatus.LIKED ? "Most Liked" : "Newest Additions"} {themes.slice(0, 2).map((theme: Theme) => ( @@ -305,7 +322,7 @@ function ThemeTab() { }}> Themes -
+
setContent(v); return ( -
+
(your theme will be reviewed and can take up to 24 hours to be approved) - + diff --git a/src/equicordplugins/themeLibrary/components/styles.css b/src/equicordplugins/themeLibrary/components/styles.css index e1d943c5..c72ab412 100644 --- a/src/equicordplugins/themeLibrary/components/styles.css +++ b/src/equicordplugins/themeLibrary/components/styles.css @@ -1,7 +1,3 @@ -@keyframes bounce { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.2); } -} [data-tab-id="ThemeLibrary"]::before { /* stylelint-disable-next-line property-no-vendor-prefix */ @@ -51,10 +47,10 @@ .vce-text-input { display: inline-block !important; color: var(--text-normal) !important; - font-family: var(--font-code) !important; font-size: 16px !important; padding: 0.5em; border: 2px solid var(--background-tertiary); + line-height: 1.2; max-height: unset; } @@ -74,3 +70,23 @@ display: flex; width: 100%; } + +.vce-search-grid { + display: grid; + height: 40px; + gap: 10px; + grid-template-columns: 1fr 200px; +} + +.vce-button { + white-space: normal; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.vce-overwrite-modal { + border: 1px solid var(--background-modifier-accent); + border-radius: 8px; + padding: 0.5em; +} diff --git a/src/equicordplugins/themeLibrary/index.tsx b/src/equicordplugins/themeLibrary/index.tsx index 48e4c5fb..6378ec17 100644 --- a/src/equicordplugins/themeLibrary/index.tsx +++ b/src/equicordplugins/themeLibrary/index.tsx @@ -8,7 +8,7 @@ import { EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; import { SettingsRouter } from "@webpack/common"; -import { settings } from "./settings"; +import { settings } from "./utils/settings"; export default definePlugin({ name: "ThemeLibrary", diff --git a/src/equicordplugins/themeLibrary/native.ts b/src/equicordplugins/themeLibrary/native.ts new file mode 100644 index 00000000..e0399362 --- /dev/null +++ b/src/equicordplugins/themeLibrary/native.ts @@ -0,0 +1,25 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { IpcMainInvokeEvent } from "electron"; +import { existsSync, type PathLike, writeFileSync } from "fs"; +import { join } from "path"; + +import type { Theme } from "./types"; + +export async function themeExists(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) { + return existsSync(join(dir.toString(), `${theme.name}.theme.css`)); +} + +export function getThemesDir(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) { + return join(dir.toString(), `${theme.name}.theme.css`); +} + +export async function downloadTheme(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) { + if (!theme.content || !theme.name) return; + const path = join(dir.toString(), `${theme.name}.theme.css`); + writeFileSync(path, Buffer.from(theme.content, "base64")); +} diff --git a/src/equicordplugins/themeLibrary/types.ts b/src/equicordplugins/themeLibrary/types.ts index c7a6f75e..70abbd70 100644 --- a/src/equicordplugins/themeLibrary/types.ts +++ b/src/equicordplugins/themeLibrary/types.ts @@ -10,20 +10,16 @@ import { User } from "discord-types/general"; export interface Theme { id: string; name: string; - file_name: string; content: string; type: string | "theme" | "snippet"; description: string; - external_url?: string; - download_url: string; - version?: string; + version: string; author: { github_name?: string; discord_name: string; discord_snowflake: string; }; - likes?: number; - downloads?: number; + likes: number; tags: string[]; thumbnail_url: string; release_date: string; @@ -31,7 +27,6 @@ export interface Theme { name: string; snowflake: string; invite_link: string; - avatar_hash: string; }; source?: string; } diff --git a/src/equicordplugins/themeLibrary/utils/Icons.tsx b/src/equicordplugins/themeLibrary/utils/Icons.tsx new file mode 100644 index 00000000..791b92bb --- /dev/null +++ b/src/equicordplugins/themeLibrary/utils/Icons.tsx @@ -0,0 +1,32 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export const LikeIcon = (isLiked: boolean) => ( + +); + +export const DownloadIcon = (props: any) => ( + +); diff --git a/src/equicordplugins/themeLibrary/auth.tsx b/src/equicordplugins/themeLibrary/utils/auth.tsx similarity index 98% rename from src/equicordplugins/themeLibrary/auth.tsx rename to src/equicordplugins/themeLibrary/utils/auth.tsx index 6e214f6b..a38142cf 100644 --- a/src/equicordplugins/themeLibrary/auth.tsx +++ b/src/equicordplugins/themeLibrary/utils/auth.tsx @@ -9,7 +9,7 @@ import { showNotification } from "@api/Notifications"; import { openModal } from "@utils/modal"; import { OAuth2AuthorizeModal, Toasts, UserStore } from "@webpack/common"; -import { logger } from "./components/LikesComponent"; +import { logger } from "../components/LikesComponent"; export async function authorizeUser(triggerModal: boolean = true) { const isAuthorized = await getAuthorization(); diff --git a/src/equicordplugins/themeLibrary/settings.tsx b/src/equicordplugins/themeLibrary/utils/settings.tsx similarity index 100% rename from src/equicordplugins/themeLibrary/settings.tsx rename to src/equicordplugins/themeLibrary/utils/settings.tsx