This commit is contained in:
thororen1234 2024-10-10 17:46:15 -04:00
parent 418568097c
commit b26faf2e37
10 changed files with 956 additions and 479 deletions

View file

@ -8,6 +8,7 @@ import "./style.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants"; import { EquicordDevs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { waitFor } from "@webpack"; import { waitFor } from "@webpack";
import { UserStore } from "@webpack/common"; import { UserStore } from "@webpack/common";
@ -15,6 +16,7 @@ import { UserStore } from "@webpack/common";
let ChannelTextAreaClasses; let ChannelTextAreaClasses;
let shouldShowColorEffects: boolean; let shouldShowColorEffects: boolean;
let position: boolean; let position: boolean;
let forceLeft = false;
waitFor(["buttonContainer", "channelTextArea"], m => (ChannelTextAreaClasses = m)); waitFor(["buttonContainer", "channelTextArea"], m => (ChannelTextAreaClasses = m));
@ -68,7 +70,7 @@ export default definePlugin({
charCounterDiv = document.createElement("div"); charCounterDiv = document.createElement("div");
charCounterDiv.classList.add("char-counter"); charCounterDiv.classList.add("char-counter");
if (position) charCounterDiv.classList.add("left"); if (position || forceLeft) charCounterDiv.classList.add("left");
charCounterDiv.innerHTML = `<span class="char-count">0</span>/<span class="char-max">${charMax}</span>`; charCounterDiv.innerHTML = `<span class="char-count">0</span>/<span class="char-max">${charMax}</span>`;
} }
@ -120,7 +122,10 @@ export default definePlugin({
const observeDOMChanges = () => { const observeDOMChanges = () => {
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
const chatTextArea = document.querySelector(`.${ChannelTextAreaClasses?.channelTextArea}`); const chatTextArea = document.querySelector(`.${ChannelTextAreaClasses?.channelTextArea}`);
if (chatTextArea) { if (chatTextArea && !document.querySelector(".char-counter")) {
const currentChannel = getCurrentChannel();
forceLeft = currentChannel?.rateLimitPerUser !== 0;
addCharCounter(); addCharCounter();
} }
}); });

View file

@ -3,7 +3,7 @@
color: var(--text-muted); color: var(--text-muted);
text-align: right; text-align: right;
position: absolute; position: absolute;
bottom: 0; bottom: -15px;
right: 0; right: 0;
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;

View file

@ -5,16 +5,13 @@
*/ */
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
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 type { Theme, ThemeLikeProps } from "../types"; import type { Theme, ThemeLikeProps } from "../types";
import { isAuthorized } from "../utils/auth"; import { isAuthorized } from "../utils/auth";
import { LikeIcon } from "../utils/Icons"; import { LikeIcon } from "../utils/Icons";
import { themeRequest } from "./ThemeTab"; import { logger, themeRequest } from "./ThemeTab";
export const logger = new Logger("ThemeLibrary", "#e5c890");
export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { themeId: Theme["id"], likedThemes: ThemeLikeProps | undefined; }) => { export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { themeId: Theme["id"], likedThemes: ThemeLikeProps | undefined; }) => {
const [likesCount, setLikesCount] = useState(0); const [likesCount, setLikesCount] = useState(0);
@ -35,7 +32,7 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
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);
const currentUser: User = UserStore.getCurrentUser(); const currentUser: User = UserStore.getCurrentUser();
const hasLiked: boolean = theme?.userIds.includes(currentUser.id) ?? false; const hasLiked: boolean = (theme?.userIds.includes(currentUser.id) || themeId === "preview") ?? false;
const endpoint = hasLiked ? "/likes/remove" : "/likes/add"; const endpoint = hasLiked ? "/likes/remove" : "/likes/add";
const token = await DataStore.get("ThemeLibrary_uniqueToken"); const token = await DataStore.get("ThemeLibrary_uniqueToken");
@ -83,9 +80,10 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
size={Button.Sizes.MEDIUM} size={Button.Sizes.MEDIUM}
color={Button.Colors.PRIMARY} color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED} look={Button.Looks.OUTLINED}
disabled={themeId === "preview"}
style={{ marginLeft: "8px" }} style={{ marginLeft: "8px" }}
> >
{LikeIcon(hasLiked)} {likesCount} {LikeIcon(hasLiked || themeId === "preview")} {themeId === "preview" ? 143 : likesCount}
</Button> </Button>
</div> </div>
); );

View file

@ -0,0 +1,180 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { generateId } from "@api/Commands";
import { Settings } from "@api/Settings";
import { OpenExternalIcon } from "@components/Icons";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Button, Card, FluxDispatcher, Forms, Parser, React, UserStore, UserUtils } from "@webpack/common";
import { User } from "discord-types/general";
import { Constructor } from "type-fest";
import type { Theme, ThemeLikeProps } from "../types";
import { LikesComponent } from "./LikesComponent";
import { ThemeInfoModal } from "./ThemeInfoModal";
import { apiUrl } from "./ThemeTab";
interface ThemeCardProps {
theme: Theme;
themeLinks: string[];
likedThemes?: ThemeLikeProps;
setThemeLinks: (links: string[]) => void;
removePreview?: boolean;
removeButtons?: boolean;
}
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
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;
}
export const ThemeCard: React.FC<ThemeCardProps> = ({ theme, themeLinks, likedThemes, setThemeLinks, removeButtons, removePreview }) => {
const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
const handleAddRemoveTheme = () => {
const onlineThemeLinks = themeLinks.includes(`${apiUrl}/${theme.name}`)
? themeLinks.filter(link => link !== `${apiUrl}/${theme.name}`)
: [...themeLinks, `${apiUrl}/${theme.name}`];
setThemeLinks(onlineThemeLinks);
Vencord.Settings.themeLinks = onlineThemeLinks;
};
const handleThemeAttributesCheck = () => {
const requiresThemeAttributes = theme.requiresThemeAttributes ?? false;
if (requiresThemeAttributes && !Settings.plugins.ThemeAttributes.enabled) {
openModal(modalProps => (
<ModalRoot {...modalProps} size={ModalSize.SMALL}>
<ModalHeader>
<Forms.FormTitle tag="h4">Hold on!</Forms.FormTitle>
</ModalHeader>
<ModalContent>
<Forms.FormText style={{ padding: "8px" }}>
<p>This theme requires the <b>ThemeAttributes</b> plugin to work properly!</p>
<p>Do you want to enable it?</p>
</Forms.FormText>
</ModalContent>
<ModalFooter>
<Button
look={Button.Looks.FILLED}
color={Button.Colors.GREEN}
onClick={() => {
Settings.plugins.ThemeAttributes.enabled = true;
modalProps.onClose();
handleAddRemoveTheme();
}}
>
Enable Plugin
</Button>
<Button
color={Button.Colors.RED}
look={Button.Looks.FILLED}
className={Margins.right8}
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>
));
} else {
handleAddRemoveTheme();
}
};
const handleViewSource = () => {
const content = window.atob(theme.content);
const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
const source = metadata.match(/@source\s+(.+)/)?.[1] || "";
if (source) {
VencordNative.native.openExternal(source);
} else {
VencordNative.native.openExternal(`${apiUrl}/${theme.name}`);
}
};
return (
<Card style={{ padding: ".5rem", marginBottom: ".5em", marginTop: ".5em", display: "flex", flexDirection: "column", backgroundColor: "var(--background-secondary-alt)" }} key={theme.id}>
<Forms.FormTitle tag="h2" style={{ overflowWrap: "break-word", marginTop: 8 }} className="vce-theme-text">
{theme.name}
</Forms.FormTitle>
<Forms.FormText className="vce-theme-text">
{Parser.parse(theme.description)}
</Forms.FormText>
{!removePreview && (
<img role="presentation" src={theme.thumbnail_url} loading="lazy" alt={theme.name} className="vce-theme-info-preview" />
)}
<div className="vce-theme-info">
<div style={{ justifyContent: "flex-start", flexDirection: "column" }}>
{theme.tags && (
<Forms.FormText>
{theme.tags.map(tag => (
<span className="vce-theme-info-tag" key={tag}>
{tag}
</span>
))}
</Forms.FormText>
)}
{!removeButtons && (
< div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
<Button
onClick={handleThemeAttributesCheck}
size={Button.Sizes.MEDIUM}
color={themeLinks.includes(`${apiUrl}/${theme.name}`) ? Button.Colors.RED : Button.Colors.GREEN}
look={Button.Looks.FILLED}
className={Margins.right8}
disabled={!theme.content || theme.id === "preview"}
>
{themeLinks.includes(`${apiUrl}/${theme.name}`) ? "Remove Theme" : "Add Theme"}
</Button>
<Button
onClick={async () => {
const authors = Array.isArray(theme.author)
? await Promise.all(theme.author.map(author => getUser(author.discord_snowflake, author.discord_name)))
: [await getUser(theme.author.discord_snowflake, theme.author.discord_name)];
openModal(props => <ThemeInfoModal {...props} author={authors} theme={theme} />);
}}
size={Button.Sizes.MEDIUM}
color={Button.Colors.BRAND}
look={Button.Looks.FILLED}
>
Theme Info
</Button>
<LikesComponent themeId={theme.id} likedThemes={likedThemes} />
<Button
onClick={handleViewSource}
size={Button.Sizes.MEDIUM}
color={Button.Colors.LINK}
look={Button.Looks.LINK}
disabled={!theme.content || theme.id === "preview"}
style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
>
View Source <OpenExternalIcon height={16} width={16} />
</Button>
</div>
)}
</div>
</div>
</Card >
);
};

View file

@ -16,7 +16,7 @@ import { Button, Clipboard, Forms, Parser, React, showToast, Toasts } from "@web
import { Theme, ThemeInfoModalProps } from "../types"; import { Theme, ThemeInfoModalProps } from "../types";
import { ClockIcon, DownloadIcon, WarningIcon } from "../utils/Icons"; import { ClockIcon, DownloadIcon, WarningIcon } from "../utils/Icons";
import { logger } from "./LikesComponent"; import { logger } from "./ThemeTab";
const Native = VencordNative.pluginHelpers.ThemeLibrary as PluginNative<typeof import("../native")>; const Native = VencordNative.pluginHelpers.ThemeLibrary as PluginNative<typeof import("../native")>;
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
@ -25,7 +25,7 @@ async function downloadTheme(themesDir: string, theme: Theme) {
try { try {
await Native.downloadTheme(themesDir, theme); await Native.downloadTheme(themesDir, theme);
showToast(`Downloaded ${theme.name}!`, Toasts.Type.SUCCESS); showToast(`Downloaded ${theme.name}!`, Toasts.Type.SUCCESS);
} catch (err: any) { } catch (err: unknown) {
logger.error(err); logger.error(err);
showToast(`Failed to download ${theme.name}! (check console)`, Toasts.Type.FAILURE); showToast(`Failed to download ${theme.name}! (check console)`, Toasts.Type.FAILURE);
} }
@ -121,34 +121,36 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
)} )}
<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
<ModalRoot {...modalProps} size={ModalSize.LARGE}> disabled={!theme.content || theme.id === "preview"}
<ModalHeader> onClick={() => openModal(modalProps => (
<Forms.FormTitle tag="h4">Theme Source</Forms.FormTitle> <ModalRoot {...modalProps} size={ModalSize.LARGE}>
</ModalHeader> <ModalHeader>
<ModalContent> <Forms.FormTitle tag="h4">Theme Source</Forms.FormTitle>
<Forms.FormText style={{ </ModalHeader>
padding: "8px", <ModalContent>
}}> <Forms.FormText style={{
<CodeBlock lang="css" content={themeContent} /> padding: "8px",
</Forms.FormText> }}>
</ModalContent> <CodeBlock lang="css" content={themeContent} />
<ModalFooter> </Forms.FormText>
<Button </ModalContent>
color={Button.Colors.RED} <ModalFooter>
look={Button.Looks.OUTLINED} <Button
onClick={() => modalProps.onClose()} color={Button.Colors.RED}
> look={Button.Looks.OUTLINED}
Close onClick={() => modalProps.onClose()}
</Button> >
<Button className={Margins.right8} Close
onClick={() => { </Button>
Clipboard.copy(themeContent); <Button className={Margins.right8}
showToast("Copied to Clipboard", Toasts.Type.SUCCESS); onClick={() => {
}}>Copy to Clipboard</Button> Clipboard.copy(themeContent);
</ModalFooter> showToast("Copied to Clipboard", Toasts.Type.SUCCESS);
</ModalRoot> }}>Copy to Clipboard</Button>
))} </ModalFooter>
</ModalRoot>
))}
> >
View Theme Source View Theme Source
</Button> </Button>
@ -190,6 +192,7 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
color={Button.Colors.GREEN} color={Button.Colors.GREEN}
look={Button.Looks.OUTLINED} look={Button.Looks.OUTLINED}
className={classes("vce-button", Margins.right8)} className={classes("vce-button", Margins.right8)}
disabled={!theme.content || theme.id === "preview"}
onClick={async () => { onClick={async () => {
const themesDir = await VencordNative.themes.getThemesDir(); const themesDir = await VencordNative.themes.getThemesDir();
const exists = await Native.themeExists(themesDir, theme); const exists = await Native.themeExists(themesDir, theme);

File diff suppressed because it is too large Load diff

View file

@ -89,3 +89,41 @@
border-radius: 8px; border-radius: 8px;
padding: 0.5em; padding: 0.5em;
} }
.vce-image-paste {
border-radius: 8px;
border: 3px dashed var(--background-modifier-accent);
background-color: var(--background-secondary);
padding: 20px;
text-align: center;
position: relative;
cursor: pointer;
margin-top: 20px;
transition: border 0.3s ease;
}
.vce-image-paste:hover {
border: 3px dashed var(--brand-500);
transition: border 0.3s ease;
}
.vce-styled-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.vce-styled-list li {
padding: 10px 0;
border-bottom: thin solid var(--background-modifier-accent);
}
.vce-styled-list li:last-child {
border-bottom: none;
}
.vce-divider-border {
border-bottom: thin solid var(--background-modifier-accent);
padding: 10px 0;
width: 100%;
}

View file

@ -4,51 +4,74 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { EquicordDevs } from "@utils/constants"; import { ModalProps } from "@utils/modal";
import definePlugin from "@utils/types"; import { User } from "discord-types/general";
import { SettingsRouter } from "@webpack/common";
import { settings } from "./utils/settings"; type Author = {
github_name?: string;
discord_name: string;
discord_snowflake: string;
};
export default definePlugin({ export interface Theme {
name: "ThemeLibrary", id: string;
description: "A library of themes for Vencord.", name: string;
authors: [EquicordDevs.Fafa], content: string;
settings, type: string | "theme" | "snippet";
toolboxActions: { description: string;
"Open Theme Library": () => { version: string;
SettingsRouter.open("ThemeLibrary"); author: Author | Author[];
}, likes: number;
}, tags: string[];
thumbnail_url: string;
release_date: Date;
last_updated?: Date;
guild?: {
name: string;
snowflake: string;
invite_link: string;
};
source?: string;
requiresThemeAttributes?: boolean;
}
start() { export interface ThemeInfoModalProps extends ModalProps {
const customSettingsSections = ( author: User | User[];
Vencord.Plugins.plugins.Settings as any as { theme: Theme;
customSections: ((ID: Record<string, unknown>) => any)[]; }
}
).customSections;
const ThemeSection = () => ({ export const enum TabItem {
section: "ThemeLibrary", THEMES,
label: "Theme Library", SUBMIT_THEMES,
element: require("./components/ThemeTab").default, }
id: "ThemeSection",
});
customSettingsSections.push(ThemeSection); export interface LikesComponentProps {
}, theme: Theme;
userId: User["id"];
}
stop() { export const enum SearchStatus {
const customSettingsSections = ( ALL,
Vencord.Plugins.plugins.Settings as any as { ENABLED,
customSections: ((ID: Record<string, unknown>) => any)[]; DISABLED,
} THEME,
).customSections; SNIPPET,
DARK,
LIGHT,
LIKED,
}
const i = customSettingsSections.findIndex( export type ThemeLikeProps = {
section => section({}).id === "ThemeSection" status: number;
); likes: [{
themeId: number;
userIds: User["id"][];
}];
};
if (i !== -1) customSettingsSections.splice(i, 1); export interface Contributor {
}, username: User["username"];
}); github_username: string;
id: User["id"];
avatar: string;
}

View file

@ -68,3 +68,10 @@ export type ThemeLikeProps = {
userIds: User["id"][]; userIds: User["id"][];
}]; }];
}; };
export interface Contributor {
username: User["username"];
github_username: string;
id: User["id"];
avatar: string;
}

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, themeRequest } from "../components/ThemeTab";
export async function authorizeUser(triggerModal: boolean = true) { export async function authorizeUser(triggerModal: boolean = true) {
const isAuthorized = await getAuthorization(); const isAuthorized = await getAuthorization();
@ -76,7 +76,7 @@ export async function deauthorizeUser() {
} }
}); });
const res = await fetch("https://themes-delta.vercel.app/api/user/revoke", { const res = await themeRequest("/user/revoke", {
method: "DELETE", method: "DELETE",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -111,7 +111,7 @@ export async function getAuthorization() {
return false; return false;
} else { } else {
// check if valid // check if valid
const res = await fetch("https://themes-delta.vercel.app/api/user/findUserByToken", { const res = await themeRequest("/user/findUserByToken", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"