Update ThemeLibrary

This commit is contained in:
thororen1234 2024-07-12 11:31:17 -04:00
parent 910274c357
commit 1e079a70e9
10 changed files with 256 additions and 95 deletions

View file

@ -9,8 +9,9 @@ import { Logger } from "@utils/Logger";
import { Button, useEffect, useRef, UserStore, useState } from "@webpack/common"; import { Button, useEffect, useRef, UserStore, useState } from "@webpack/common";
import type { User } from "discord-types/general"; import type { User } from "discord-types/general";
import { isAuthorized } from "../auth";
import type { Theme, ThemeLikeProps } from "../types"; import type { Theme, ThemeLikeProps } from "../types";
import { isAuthorized } from "../utils/auth";
import { LikeIcon } from "../utils/Icons";
import { themeRequest } from "./ThemeTab"; import { themeRequest } from "./ThemeTab";
export const logger = new Logger("ThemeLibrary", "#e5c890"); export const logger = new Logger("ThemeLibrary", "#e5c890");
@ -30,12 +31,6 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
return themeLike ? themeLike.userIds.length : 0; return themeLike ? themeLike.userIds.length : 0;
} }
const likeIcon = (isLiked: boolean) => (
<svg viewBox="0 0 20 20" fill={isLiked ? "red" : "currentColor"} aria-hidden="true" width="18" height="18" className="vce-likes-icon vce-likes-icon-animation">
<path d="M16.44 3.10156C14.63 3.10156 13.01 3.98156 12 5.33156C10.99 3.98156 9.37 3.10156 7.56 3.10156C4.49 3.10156 2 5.60156 2 8.69156C2 9.88156 2.19 10.9816 2.52 12.0016C4.1 17.0016 8.97 19.9916 11.38 20.8116C11.72 20.9316 12.28 20.9316 12.62 20.8116C15.03 19.9916 19.9 17.0016 21.48 12.0016C21.81 10.9816 22 9.88156 22 8.69156C22 5.60156 19.51 3.10156 16.44 3.10156Z" />
</svg>
);
const handleLikeClick = async (themeId: Theme["id"]) => { const handleLikeClick = async (themeId: Theme["id"]) => {
if (!isAuthorized()) return; if (!isAuthorized()) return;
const theme = likedThemes?.likes.find(like => like.themeId === themeId as unknown as Number); 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} look={Button.Looks.OUTLINED}
style={{ marginLeft: "8px" }} style={{ marginLeft: "8px" }}
> >
{likeIcon(hasLiked)} {likesCount} {LikeIcon(hasLiked)} {likesCount}
</Button> </Button>
</div> </div>
); );

View file

@ -8,21 +8,37 @@ import { CodeBlock } from "@components/CodeBlock";
import { Heart } from "@components/Heart"; import { Heart } from "@components/Heart";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import type { PluginNative } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { Button, Clipboard, Forms, React, showToast, Toasts } from "@webpack/common"; 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<typeof import("../native")>;
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ 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<ThemeInfoModalProps> = ({ author, theme, ...props }) => {
const content = window.atob(theme.content); const content = window.atob(theme.content);
const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || ""; const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
const donate = metadata.match(/@donate\s+(.+)/)?.[1] || ""; const donate = metadata.match(/@donate\s+(.+)/)?.[1] || "";
const version = metadata.match(/@version\s+(.+)/)?.[1] || ""; const version = metadata.match(/@version\s+(.+)/)?.[1] || "";
const { likes, guild, tags } = theme;
return ( return (
<ModalRoot {...props}> <ModalRoot {...props}>
<ModalHeader> <ModalHeader>
@ -48,6 +64,53 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
{author.username} {author.username}
</Forms.FormText> </Forms.FormText>
</div> </div>
{version && (
<>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Version</Forms.FormTitle>
<Forms.FormText>
{version}
</Forms.FormText>
</>
)}
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Likes</Forms.FormTitle>
<Forms.FormText>
{likes === 0 ? `Nobody liked this ${theme.type} yet.` : `${likes} users liked this ${theme.type}!`}
</Forms.FormText>
{donate && (
<>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Donate</Forms.FormTitle>
<Forms.FormText>
You can support the author by donating below!
</Forms.FormText>
<Forms.FormText style={{ marginTop: "10px" }}>
<Button onClick={() => VencordNative.native.openExternal(donate)}>
<Heart />
Donate
</Button>
</Forms.FormText>
</>
)}
{guild && (
<>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Support Server</Forms.FormTitle>
<Forms.FormText>
{guild.name}
</Forms.FormText>
<Forms.FormText>
<Button
color={Button.Colors.BRAND_NEW}
look={Button.Looks.FILLED}
className={Margins.top8}
onClick={async e => {
e.preventDefault();
guild.invite_link != null && openInviteModal(guild.invite_link.split("discord.gg/")[1]).catch(() => showToast("Invalid or expired invite!", Toasts.Type.FAILURE));
}}
>
Join Discord Server
</Button>
</Forms.FormText>
</>
)}
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Source</Forms.FormTitle> <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Source</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
<Button onClick={() => openModal(modalProps => ( <Button onClick={() => openModal(modalProps => (
@ -57,7 +120,7 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
</ModalHeader> </ModalHeader>
<ModalContent> <ModalContent>
<Forms.FormText style={{ <Forms.FormText style={{
padding: "5px", padding: "8px",
}}> }}>
<CodeBlock lang="css" content={content} /> <CodeBlock lang="css" content={content} />
</Forms.FormText> </Forms.FormText>
@ -82,54 +145,11 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
View Theme Source View Theme Source
</Button> </Button>
</Forms.FormText> </Forms.FormText>
{version && ( {tags && (
<>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Version</Forms.FormTitle>
<Forms.FormText>
{version}
</Forms.FormText>
</>
)}
{donate && (
<>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Donate</Forms.FormTitle>
<Forms.FormText>
You can support the author by donating below.
</Forms.FormText>
<Forms.FormText style={{ marginTop: "10px" }}>
<Button onClick={() => VencordNative.native.openExternal(donate)}>
<Heart />
Donate
</Button>
</Forms.FormText>
</>
)}
{theme.guild && (
<>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Support Server</Forms.FormTitle>
<Forms.FormText>
{theme.guild.name}
</Forms.FormText>
<Forms.FormText>
<Button
color={Button.Colors.BRAND_NEW}
look={Button.Looks.FILLED}
className={Margins.top8}
onClick={async e => {
e.preventDefault();
theme.guild?.invite_link != null && openInviteModal(theme.guild?.invite_link.split("discord.gg/")[1]).catch(() => showToast("Invalid or expired invite!", Toasts.Type.FAILURE));
}}
>
Join Discord Server
</Button>
</Forms.FormText>
</>
)}
{theme.tags && (
<> <>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Tags</Forms.FormTitle> <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Tags</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
{theme.tags.map(tag => ( {tags.map(tag => (
<span className="vce-theme-info-tag"> <span className="vce-theme-info-tag">
{tag} {tag}
</span> </span>
@ -148,6 +168,68 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
> >
Close Close
</Button> </Button>
<Button
color={Button.Colors.GREEN}
look={Button.Looks.OUTLINED}
className={classes("vce-button", Margins.right8)}
onClick={async () => {
const themesDir = await VencordNative.themes.getThemesDir();
const exists = await Native.themeExists(themesDir, theme);
// using another function so we get the proper file path instead of just guessing
// which slash to use (im looking at you windows)
const validThemesDir = await Native.getThemesDir(themesDir, theme);
// check if theme exists, and ask if they want to overwrite
if (exists) {
showToast("A file with the same name already exists!", Toasts.Type.FAILURE);
openModal(modalProps => (
<ModalRoot {...modalProps} size={ModalSize.SMALL}>
<ModalHeader>
<Forms.FormTitle tag="h4">Conflict!</Forms.FormTitle>
</ModalHeader>
<ModalContent>
<Forms.FormText style={{
padding: "8px",
}}>
<div style={{ display: "flex", flexDirection: "column" }}>
<p>A theme with the same name <b>already exist</b> in your themes directory! Do you want to overwrite it?</p>
<div className="vce-overwrite-modal">
<code style={{ wordWrap: "break-word" }}>
{validThemesDir}
</code>
</div>
</div>
</Forms.FormText>
</ModalContent>
<ModalFooter>
<Button
look={Button.Looks.FILLED}
color={Button.Colors.RED}
onClick={async () => {
await downloadTheme(themesDir, theme);
modalProps.onClose();
}}
>
Overwrite
</Button>
<Button
color={Button.Colors.GREEN}
look={Button.Looks.FILLED}
className={Margins.right8} onClick={() => modalProps.onClose()}
>
Keep my file
</Button>
</ModalFooter>
</ModalRoot>
));
} else {
await downloadTheme(themesDir, theme);
}
}}
>
<div style={{ display: "flex", alignItems: "center" }}>
Download <DownloadIcon style={{ marginLeft: "5px" }} />
</div>
</Button>
</ModalFooter> </ModalFooter>
</ModalRoot> </ModalRoot>
); );

View file

@ -10,7 +10,6 @@ import "./styles.css";
import { generateId } from "@api/Commands"; import { generateId } from "@api/Commands";
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { CodeBlock } from "@components/CodeBlock"; import { CodeBlock } from "@components/CodeBlock";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { OpenExternalIcon } from "@components/Icons"; 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 { User } from "discord-types/general";
import { Constructor } from "type-fest"; import { Constructor } from "type-fest";
import { isAuthorized } from "../auth";
import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types"; import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
import { isAuthorized } from "../utils/auth";
import { LikesComponent } from "./LikesComponent"; import { LikesComponent } from "./LikesComponent";
import { ThemeInfoModal } from "./ThemeInfoModal"; import { ThemeInfoModal } from "./ThemeInfoModal";
const cl = classNameFactory("vc-plugins-");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error"); const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error");
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any; const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
const TextAreaProps = findLazy(m => typeof m.textarea === "string"); const TextAreaProps = findLazy(m => typeof m.textarea === "string");
@ -88,6 +86,15 @@ const themeTemplate = `/**
/* Your CSS goes here */ /* Your CSS goes here */
`; `;
const SearchTags = {
[SearchStatus.THEME]: "THEME",
[SearchStatus.SNIPPET]: "SNIPPET",
[SearchStatus.LIKED]: "LIKED",
[SearchStatus.DARK]: "DARK",
[SearchStatus.LIGHT]: "LIGHT",
};
function ThemeTab() { function ThemeTab() {
const [themes, setThemes] = useState<Theme[]>([]); const [themes, setThemes] = useState<Theme[]>([]);
const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]); const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]);
@ -103,12 +110,15 @@ function ThemeTab() {
const themeFilter = (theme: Theme) => { const themeFilter = (theme: Theme) => {
const enabled = themeLinks.includes(`${API_URL}/${theme.name}`); const enabled = themeLinks.includes(`${API_URL}/${theme.name}`);
if (enabled && searchValue.status === SearchStatus.DISABLED) return false; const tags = new Set(theme.tags.map(tag => tag.toLowerCase()));
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 (!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; if (!searchValue.value.length) return true;
const v = searchValue.value.toLowerCase(); const v = searchValue.value.toLowerCase();
@ -116,7 +126,7 @@ function ThemeTab() {
theme.name.toLowerCase().includes(v) || theme.name.toLowerCase().includes(v) ||
theme.description.toLowerCase().includes(v) || theme.description.toLowerCase().includes(v) ||
theme.author.discord_name.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(() => { useEffect(() => {
const fetchThemes = async () => { const fetchData = async () => {
try { try {
const themes = await fetchAllThemes(); const [themes, likes] = await Promise.all([fetchAllThemes(), fetchLikes()]);
// fetch likes
setThemes(themes); setThemes(themes);
const likes = await fetchLikes();
setLikedThemes(likes); setLikedThemes(likes);
setFilteredThemes(themes); setFilteredThemes(themes);
} catch (err) { } catch (err) {
@ -145,7 +153,7 @@ function ThemeTab() {
setLoading(false); setLoading(false);
} }
}; };
fetchThemes(); fetchData();
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -153,9 +161,18 @@ function ThemeTab() {
}, [Vencord.Settings.themeLinks]); }, [Vencord.Settings.themeLinks]);
useEffect(() => { useEffect(() => {
const filteredThemes = themes.filter(themeFilter); // likes only update after 12_000 due to cache
setFilteredThemes(filteredThemes); if (searchValue.status === SearchStatus.LIKED) {
}, [searchValue]); 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 ( return (
<div> <div>
@ -191,13 +208,13 @@ function ThemeTab() {
>Hide</Button> >Hide</Button>
</ErrorCard > </ErrorCard >
)} )}
<div className={`${Margins.bottom8} ${Margins.top16}`}> <div className={classes(Margins.bottom8, Margins.top16)}>
<Forms.FormTitle tag="h2" <Forms.FormTitle tag="h2"
style={{ style={{
overflowWrap: "break-word", overflowWrap: "break-word",
marginTop: 8, marginTop: 8,
}}> }}>
Newest Additions {searchValue.status === SearchStatus.LIKED ? "Most Liked" : "Newest Additions"}
</Forms.FormTitle> </Forms.FormTitle>
{themes.slice(0, 2).map((theme: Theme) => ( {themes.slice(0, 2).map((theme: Theme) => (
@ -305,7 +322,7 @@ function ThemeTab() {
}}> }}>
Themes Themes
</Forms.FormTitle> </Forms.FormTitle>
<div className={classes(Margins.bottom20, cl("filter-controls"))}> <div className={classes(Margins.bottom20, "vce-search-grid")}>
<TextInput value={searchValue.value} placeholder="Search for a theme..." onChange={onSearch} /> <TextInput value={searchValue.value} placeholder="Search for a theme..." onChange={onSearch} />
<div className={InputStyles.inputWrapper}> <div className={InputStyles.inputWrapper}>
<SearchableSelect <SearchableSelect
@ -313,8 +330,7 @@ function ThemeTab() {
{ label: "Show All", value: SearchStatus.ALL, default: true }, { label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Themes", value: SearchStatus.THEME }, { label: "Show Themes", value: SearchStatus.THEME },
{ label: "Show Snippets", value: SearchStatus.SNIPPET }, { label: "Show Snippets", value: SearchStatus.SNIPPET },
// TODO: filter for most liked themes { label: "Show Most Liked", value: SearchStatus.LIKED },
// { label: "Show Most Liked", value: SearchStatus.LIKED },
{ label: "Show Dark", value: SearchStatus.DARK }, { label: "Show Dark", value: SearchStatus.DARK },
{ label: "Show Light", value: SearchStatus.LIGHT }, { label: "Show Light", value: SearchStatus.LIGHT },
{ label: "Show Enabled", value: SearchStatus.ENABLED }, { label: "Show Enabled", value: SearchStatus.ENABLED },
@ -448,7 +464,7 @@ function SubmitThemes() {
const handleChange = (v: string) => setContent(v); const handleChange = (v: string) => setContent(v);
return ( return (
<div className={`${Margins.bottom8} ${Margins.top16}`}> <div className={classes(Margins.bottom8, Margins.top16)}>
<Forms.FormTitle tag="h2" style={{ <Forms.FormTitle tag="h2" style={{
overflowWrap: "break-word", overflowWrap: "break-word",
marginTop: 8, marginTop: 8,
@ -461,7 +477,7 @@ function SubmitThemes() {
<Forms.FormText> <Forms.FormText>
(your theme will be reviewed and can take up to 24 hours to be approved) (your theme will be reviewed and can take up to 24 hours to be approved)
</Forms.FormText> </Forms.FormText>
<Forms.FormText className={`${Margins.bottom16} ${Margins.top8}`}> <Forms.FormText className={classes(Margins.bottom16, Margins.top8)}>
<CodeBlock lang="css" content={themeTemplate} /> <CodeBlock lang="css" content={themeTemplate} />
</Forms.FormText> </Forms.FormText>
<Forms.FormTitle tag="h2" style={{ <Forms.FormTitle tag="h2" style={{
@ -478,7 +494,7 @@ function SubmitThemes() {
content={themeTemplate} content={themeTemplate}
onChange={handleChange} onChange={handleChange}
className={classes(TextAreaProps.textarea, "vce-text-input")} className={classes(TextAreaProps.textarea, "vce-text-input")}
placeholder="Theme CSS goes here..." placeholder={themeTemplate}
spellCheck={false} spellCheck={false}
rows={35} rows={35}
/> />

View file

@ -1,7 +1,3 @@
@keyframes bounce {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
[data-tab-id="ThemeLibrary"]::before { [data-tab-id="ThemeLibrary"]::before {
/* stylelint-disable-next-line property-no-vendor-prefix */ /* stylelint-disable-next-line property-no-vendor-prefix */
@ -51,10 +47,10 @@
.vce-text-input { .vce-text-input {
display: inline-block !important; display: inline-block !important;
color: var(--text-normal) !important; color: var(--text-normal) !important;
font-family: var(--font-code) !important;
font-size: 16px !important; font-size: 16px !important;
padding: 0.5em; padding: 0.5em;
border: 2px solid var(--background-tertiary); border: 2px solid var(--background-tertiary);
line-height: 1.2;
max-height: unset; max-height: unset;
} }
@ -74,3 +70,23 @@
display: flex; display: flex;
width: 100%; 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;
}

View file

@ -8,7 +8,7 @@ import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { SettingsRouter } from "@webpack/common"; import { SettingsRouter } from "@webpack/common";
import { settings } from "./settings"; import { settings } from "./utils/settings";
export default definePlugin({ export default definePlugin({
name: "ThemeLibrary", name: "ThemeLibrary",

View file

@ -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"));
}

View file

@ -10,20 +10,16 @@ import { User } from "discord-types/general";
export interface Theme { export interface Theme {
id: string; id: string;
name: string; name: string;
file_name: string;
content: string; content: string;
type: string | "theme" | "snippet"; type: string | "theme" | "snippet";
description: string; description: string;
external_url?: string; version: string;
download_url: string;
version?: string;
author: { author: {
github_name?: string; github_name?: string;
discord_name: string; discord_name: string;
discord_snowflake: string; discord_snowflake: string;
}; };
likes?: number; likes: number;
downloads?: number;
tags: string[]; tags: string[];
thumbnail_url: string; thumbnail_url: string;
release_date: string; release_date: string;
@ -31,7 +27,6 @@ export interface Theme {
name: string; name: string;
snowflake: string; snowflake: string;
invite_link: string; invite_link: string;
avatar_hash: string;
}; };
source?: string; source?: string;
} }

View file

@ -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) => (
<svg
viewBox="0 0 20 20"
fill={isLiked ? "red" : "currentColor"}
aria-hidden="true"
width="18"
height="18"
className="vce-likes-icon"
>
<path d="M16.44 3.10156C14.63 3.10156 13.01 3.98156 12 5.33156C10.99 3.98156 9.37 3.10156 7.56 3.10156C4.49 3.10156 2 5.60156 2 8.69156C2 9.88156 2.19 10.9816 2.52 12.0016C4.1 17.0016 8.97 19.9916 11.38 20.8116C11.72 20.9316 12.28 20.9316 12.62 20.8116C15.03 19.9916 19.9 17.0016 21.48 12.0016C21.81 10.9816 22 9.88156 22 8.69156C22 5.60156 19.51 3.10156 16.44 3.10156Z" />
</svg>
);
export const DownloadIcon = (props: any) => (
<svg
aria-hidden="true"
viewBox="0 0 20 20"
width="20"
height="20"
fill="currentColor"
{...props}
>
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path>
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"></path>
</svg>
);

View file

@ -9,7 +9,7 @@ import { showNotification } from "@api/Notifications";
import { openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, Toasts, UserStore } from "@webpack/common"; import { OAuth2AuthorizeModal, Toasts, UserStore } from "@webpack/common";
import { logger } from "./components/LikesComponent"; import { logger } from "../components/LikesComponent";
export async function authorizeUser(triggerModal: boolean = true) { export async function authorizeUser(triggerModal: boolean = true) {
const isAuthorized = await getAuthorization(); const isAuthorized = await getAuthorization();