mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-21 15:48:52 -05:00
Update ThemeLibrary
This commit is contained in:
parent
ab7547e698
commit
bc0543c8dd
8 changed files with 316 additions and 89 deletions
141
src/equicordplugins/themeLibrary/auth.tsx
Normal file
141
src/equicordplugins/themeLibrary/auth.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
|
import { showNotification } from "@api/Notifications";
|
||||||
|
import { openModal } from "@utils/modal";
|
||||||
|
import { OAuth2AuthorizeModal, Toasts, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
import { logger } from "./components/LikesComponent";
|
||||||
|
|
||||||
|
export async function authorizeUser(triggerModal: boolean = true) {
|
||||||
|
const isAuthorized = await getAuthorization();
|
||||||
|
|
||||||
|
if (isAuthorized === false) {
|
||||||
|
if (!triggerModal) return false;
|
||||||
|
openModal((props: any) => <OAuth2AuthorizeModal
|
||||||
|
{...props}
|
||||||
|
scopes={["identify"]}
|
||||||
|
responseType="code"
|
||||||
|
redirectUri="https://themes-delta.vercel.app/api/user/auth"
|
||||||
|
permissions={0n}
|
||||||
|
clientId="1257819493422465235"
|
||||||
|
cancelCompletesFlow={false}
|
||||||
|
callback={async ({ location }: any) => {
|
||||||
|
if (!location) return logger.error("No redirect location returned");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(location, {
|
||||||
|
headers: { Accept: "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { token } = await response.json();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
logger.debug("Authorized via OAuth2, got token");
|
||||||
|
await DataStore.set("ThemeLibrary_uniqueToken", token);
|
||||||
|
showNotification({
|
||||||
|
title: "ThemeLibrary",
|
||||||
|
body: "Successfully authorized with ThemeLibrary!"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.debug("Tried to authorize via OAuth2, but no token returned");
|
||||||
|
showNotification({
|
||||||
|
title: "ThemeLibrary",
|
||||||
|
body: "Failed to authorize, check console"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error("Failed to authorize", e);
|
||||||
|
showNotification({
|
||||||
|
title: "ThemeLibrary",
|
||||||
|
body: "Failed to authorize, check console"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>);
|
||||||
|
} else {
|
||||||
|
return isAuthorized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deauthorizeUser() {
|
||||||
|
const uniqueToken = await DataStore.get<Record<string, string>>("ThemeLibrary_uniqueToken");
|
||||||
|
|
||||||
|
if (!uniqueToken) return Toasts.show({
|
||||||
|
message: "No uniqueToken present, try authorizing first!",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
options: {
|
||||||
|
duration: 2e3,
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch("https://themes-delta.vercel.app/api/user/revoke", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token: uniqueToken, userId: UserStore.getCurrentUser().id })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
await DataStore.del("ThemeLibrary_uniqueToken");
|
||||||
|
showNotification({
|
||||||
|
title: "ThemeLibrary",
|
||||||
|
body: "Successfully deauthorized from ThemeLibrary!"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// try to delete anyway
|
||||||
|
try {
|
||||||
|
await DataStore.del("ThemeLibrary_uniqueToken");
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to delete token", e);
|
||||||
|
showNotification({
|
||||||
|
title: "ThemeLibrary",
|
||||||
|
body: "Failed to deauthorize, check console"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuthorization() {
|
||||||
|
const uniqueToken = await DataStore.get<Record<string, string>>("ThemeLibrary_uniqueToken");
|
||||||
|
|
||||||
|
if (!uniqueToken) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// check if valid
|
||||||
|
const res = await fetch("https://themes-delta.vercel.app/api/user/findUserByToken", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token: uniqueToken })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 400 || res.status === 500) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.debug("User is already authorized, skipping");
|
||||||
|
return uniqueToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isAuthorized(triggerModal: boolean = true) {
|
||||||
|
const isAuthorized = await getAuthorization();
|
||||||
|
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
||||||
|
|
||||||
|
if (isAuthorized === false || !token) {
|
||||||
|
await authorizeUser(triggerModal);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,32 +4,24 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Button, useEffect, 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 { themeRequest } from "./ThemeTab";
|
import { themeRequest } from "./ThemeTab";
|
||||||
|
|
||||||
const logger = new Logger("ThemeLibrary", "#e5c890");
|
export const logger = new Logger("ThemeLibrary", "#e5c890");
|
||||||
|
|
||||||
const fetchLikes = async () => {
|
|
||||||
try {
|
|
||||||
const response = await themeRequest("/likes/get");
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
||||||
const [likedThemes, setLikedThemes] = useState(initialLikedThemes);
|
const [likedThemes, setLikedThemes] = useState(initialLikedThemes);
|
||||||
|
const debounce = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const likes = getThemeLikes(themeId);
|
const likes = getThemeLikes(themeId);
|
||||||
logger.debug("likes", likes, "for:", themeId);
|
|
||||||
setLikesCount(likes);
|
setLikesCount(likes);
|
||||||
}, [likedThemes, themeId]);
|
}, [likedThemes, themeId]);
|
||||||
|
|
||||||
|
@ -45,10 +37,17 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLikeClick = async (themeId: Theme["id"]) => {
|
const handleLikeClick = async (themeId: Theme["id"]) => {
|
||||||
|
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) ?? false;
|
||||||
const endpoint = hasLiked ? "/likes/remove" : "/likes/add";
|
const endpoint = hasLiked ? "/likes/remove" : "/likes/add";
|
||||||
|
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
||||||
|
|
||||||
|
// doing this so the delay is not visible to the user
|
||||||
|
if (debounce.current) return;
|
||||||
|
setLikesCount(likesCount + (hasLiked ? -1 : 1));
|
||||||
|
debounce.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await themeRequest(endpoint, {
|
const response = await themeRequest(endpoint, {
|
||||||
|
@ -56,14 +55,13 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
cache: "no-store",
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userId: currentUser.id,
|
token,
|
||||||
themeId: themeId,
|
themeId: themeId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) return logger.error("Couldnt update likes, res:", response.statusText);
|
if (!response.ok) return logger.error("Couldnt update likes, response not ok");
|
||||||
|
|
||||||
const fetchLikes = async () => {
|
const fetchLikes = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -76,11 +74,10 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchLikes();
|
fetchLikes();
|
||||||
// doing it locally isnt the best way probably, but it does the same
|
|
||||||
setLikesCount(likesCount + (hasLiked ? -1 : 1));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
}
|
}
|
||||||
|
debounce.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasLiked = likedThemes?.likes.some(like => like.themeId === themeId as unknown as Number && like.userIds.includes(UserStore.getCurrentUser().id)) ?? false;
|
const hasLiked = likedThemes?.likes.some(like => like.themeId === themeId as unknown as Number && like.userIds.includes(UserStore.getCurrentUser().id)) ?? false;
|
||||||
|
|
|
@ -18,7 +18,7 @@ const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaul
|
||||||
|
|
||||||
export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, ...props }) => {
|
export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, ...props }) => {
|
||||||
|
|
||||||
const content = 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] || "";
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { generateId } from "@api/Commands";
|
import { generateId } from "@api/Commands";
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { CodeBlock } from "@components/CodeBlock";
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
|
@ -20,16 +21,17 @@ import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { openModal } from "@utils/modal";
|
import { openModal } from "@utils/modal";
|
||||||
import { findByPropsLazy, findLazy } from "@webpack";
|
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 { Button, Card, FluxDispatcher, Forms, React, SearchableSelect, TabBar, TextArea, TextInput, Toasts, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
|
||||||
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 { LikesComponent } from "./LikesComponent";
|
import { LikesComponent } from "./LikesComponent";
|
||||||
import { ThemeInfoModal } from "./ThemeInfoModal";
|
import { ThemeInfoModal } from "./ThemeInfoModal";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-plugins-");
|
const cl = classNameFactory("vc-plugins-");
|
||||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
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");
|
||||||
|
|
||||||
|
@ -37,33 +39,18 @@ const API_URL = "https://themes-delta.vercel.app/api";
|
||||||
|
|
||||||
const logger = new Logger("ThemeLibrary", "#e5c890");
|
const logger = new Logger("ThemeLibrary", "#e5c890");
|
||||||
|
|
||||||
async function fetchThemes(url: string): Promise<Theme[]> {
|
export async function fetchAllThemes(): Promise<Theme[]> {
|
||||||
const response = await fetch(url);
|
const response = await themeRequest("/themes");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const themes: Theme[] = Object.values(data);
|
const themes: Theme[] = Object.values(data);
|
||||||
themes.forEach(theme => {
|
themes.forEach(theme => {
|
||||||
if (!theme.source) {
|
if (!theme.source) {
|
||||||
theme.source = `${API_URL}/${theme.name}`;
|
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());
|
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 = {}) {
|
export async function themeRequest(path: string, options: RequestInit = {}) {
|
||||||
return fetch(API_URL + path, {
|
return fetch(API_URL + path, {
|
||||||
...options,
|
...options,
|
||||||
|
@ -115,7 +102,7 @@ function ThemeTab() {
|
||||||
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||||
|
|
||||||
const themeFilter = (theme: Theme) => {
|
const themeFilter = (theme: Theme) => {
|
||||||
const enabled = themeLinks.includes(API_TYPE(theme));
|
const enabled = themeLinks.includes(`${API_URL}/${theme.name}`);
|
||||||
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
||||||
if (!theme.tags.includes("theme") && searchValue.status === SearchStatus.THEME) 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("snippet") && searchValue.status === SearchStatus.SNIPPET) return false;
|
||||||
|
@ -146,7 +133,7 @@ function ThemeTab() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchThemes = async () => {
|
const fetchThemes = async () => {
|
||||||
try {
|
try {
|
||||||
const themes = await API_TYPE({}, true);
|
const themes = await fetchAllThemes();
|
||||||
// fetch likes
|
// fetch likes
|
||||||
setThemes(themes);
|
setThemes(themes);
|
||||||
const likes = await fetchLikes();
|
const likes = await fetchLikes();
|
||||||
|
@ -187,7 +174,7 @@ function ThemeTab() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{hideWarningCard ? null : (
|
{hideWarningCard ? null : (
|
||||||
<ErrorCard id="vc-themetab-warning">
|
<ErrorCard>
|
||||||
<Forms.FormTitle tag="h4">Want your theme removed?</Forms.FormTitle>
|
<Forms.FormTitle tag="h4">Want your theme removed?</Forms.FormTitle>
|
||||||
<Forms.FormText className={Margins.top8}>
|
<Forms.FormText className={Margins.top8}>
|
||||||
If you want your theme(s) permanently removed, please open an issue on <a href="https://github.com/Faf4a/plugins/issues/new?labels=removal&projects=&template=request_removal.yml&title=Theme+Removal">GitHub <OpenExternalIcon height={16} width={16} /></a>
|
If you want your theme(s) permanently removed, please open an issue on <a href="https://github.com/Faf4a/plugins/issues/new?labels=removal&projects=&template=request_removal.yml&title=Theme+Removal">GitHub <OpenExternalIcon height={16} width={16} /></a>
|
||||||
|
@ -200,7 +187,7 @@ function ThemeTab() {
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
look={Button.Looks.FILLED}
|
look={Button.Looks.FILLED}
|
||||||
className={Margins.top8}
|
className={classes(Margins.top16, "vce-warning-button")}
|
||||||
>Hide</Button>
|
>Hide</Button>
|
||||||
</ErrorCard >
|
</ErrorCard >
|
||||||
)}
|
)}
|
||||||
|
@ -247,10 +234,10 @@ function ThemeTab() {
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
||||||
{themeLinks.includes(API_TYPE(theme)) ? (
|
{themeLinks.includes(`${API_URL}/${theme.name}`) ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const onlineThemeLinks = themeLinks.filter(x => x !== API_TYPE(theme));
|
const onlineThemeLinks = themeLinks.filter(x => x !== `${API_URL}/${theme.name}`);
|
||||||
setThemeLinks(onlineThemeLinks);
|
setThemeLinks(onlineThemeLinks);
|
||||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
}}
|
}}
|
||||||
|
@ -264,7 +251,7 @@ function ThemeTab() {
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const onlineThemeLinks = [...themeLinks, API_TYPE(theme)];
|
const onlineThemeLinks = [...themeLinks, `${API_URL}/${theme.name}`];
|
||||||
setThemeLinks(onlineThemeLinks);
|
setThemeLinks(onlineThemeLinks);
|
||||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
}}
|
}}
|
||||||
|
@ -289,14 +276,14 @@ function ThemeTab() {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const content = 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 source = metadata.match(/@source\s+(.+)/)?.[1] || "";
|
const source = metadata.match(/@source\s+(.+)/)?.[1] || "";
|
||||||
|
|
||||||
if (source) {
|
if (source) {
|
||||||
VencordNative.native.openExternal(source);
|
VencordNative.native.openExternal(source);
|
||||||
} else {
|
} else {
|
||||||
VencordNative.native.openExternal(API_TYPE(theme).replace("?raw=true", ""));
|
VencordNative.native.openExternal(`${API_URL}/${theme.name}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
size={Button.Sizes.MEDIUM}
|
size={Button.Sizes.MEDIUM}
|
||||||
|
@ -318,10 +305,10 @@ function ThemeTab() {
|
||||||
}}>
|
}}>
|
||||||
Themes
|
Themes
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
<div className={cl("filter-controls")}>
|
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
|
||||||
<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}>
|
||||||
<Select
|
<SearchableSelect
|
||||||
options={[
|
options={[
|
||||||
{ 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 },
|
||||||
|
@ -333,10 +320,12 @@ function ThemeTab() {
|
||||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||||
]}
|
]}
|
||||||
serialize={String}
|
// @ts-ignore
|
||||||
select={onStatusChange}
|
value={searchValue.status}
|
||||||
isSelected={v => v === searchValue.status}
|
clearable={false}
|
||||||
|
onChange={v => onStatusChange(v as SearchStatus)}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
|
className={InputStyles.inputDefault}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -382,10 +371,10 @@ function ThemeTab() {
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
||||||
{themeLinks.includes(API_TYPE(theme)) ? (
|
{themeLinks.includes(`${API_URL}/${theme.name}`) ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const onlineThemeLinks = themeLinks.filter(x => x !== API_TYPE(theme));
|
const onlineThemeLinks = themeLinks.filter(x => x !== `${API_URL}/${theme.name}`);
|
||||||
setThemeLinks(onlineThemeLinks);
|
setThemeLinks(onlineThemeLinks);
|
||||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
}}
|
}}
|
||||||
|
@ -399,7 +388,7 @@ function ThemeTab() {
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const onlineThemeLinks = [...themeLinks, API_TYPE(theme)];
|
const onlineThemeLinks = [...themeLinks, `${API_URL}/${theme.name}`];
|
||||||
setThemeLinks(onlineThemeLinks);
|
setThemeLinks(onlineThemeLinks);
|
||||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
}}
|
}}
|
||||||
|
@ -425,14 +414,14 @@ function ThemeTab() {
|
||||||
<LikesComponent themeId={theme.id} likedThemes={likedThemes} />
|
<LikesComponent themeId={theme.id} likedThemes={likedThemes} />
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const content = 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 source = metadata.match(/@source\s+(.+)/)?.[1] || "";
|
const source = metadata.match(/@source\s+(.+)/)?.[1] || "";
|
||||||
|
|
||||||
if (source) {
|
if (source) {
|
||||||
VencordNative.native.openExternal(source);
|
VencordNative.native.openExternal(source);
|
||||||
} else {
|
} else {
|
||||||
VencordNative.native.openExternal(API_TYPE(theme).replace("?raw=true", ""));
|
VencordNative.native.openExternal(`${API_URL}/${theme.name}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
size={Button.Sizes.MEDIUM}
|
size={Button.Sizes.MEDIUM}
|
||||||
|
@ -455,9 +444,7 @@ function ThemeTab() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubmitThemes() {
|
function SubmitThemes() {
|
||||||
const currentUser = UserStore.getCurrentUser();
|
|
||||||
const [themeContent, setContent] = useState("");
|
const [themeContent, setContent] = useState("");
|
||||||
|
|
||||||
const handleChange = (v: string) => setContent(v);
|
const handleChange = (v: string) => setContent(v);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -483,7 +470,7 @@ function SubmitThemes() {
|
||||||
}}>
|
}}>
|
||||||
Submit Themes
|
Submit Themes
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
<Forms.FormText>
|
<Forms.FormText className={Margins.bottom16}>
|
||||||
If you plan on updating your theme / snippet frequently, consider using an <code>@import</code> instead!
|
If you plan on updating your theme / snippet frequently, consider using an <code>@import</code> instead!
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
|
@ -497,8 +484,20 @@ function SubmitThemes() {
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (themeContent.length < 50) return showToast("Theme content is too short, must be above 50", Toasts.Type.FAILURE);
|
if (!(await isAuthorized())) return;
|
||||||
|
|
||||||
|
if (themeContent.length < 50) return Toasts.show({
|
||||||
|
message: "Failed to submit theme, content must be at least 50 characters long.",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
options: {
|
||||||
|
duration: 5e3,
|
||||||
|
position: Toasts.Position.TOP
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
||||||
|
|
||||||
themeRequest("/submit/theme", {
|
themeRequest("/submit/theme", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -506,13 +505,14 @@ function SubmitThemes() {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userId: `${currentUser.id}`,
|
token,
|
||||||
content: btoa(themeContent),
|
content: window.btoa(themeContent),
|
||||||
}),
|
}),
|
||||||
}).then(response => {
|
}).then(async response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
const res = await response.json();
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
message: "Failed to submit theme, try again later. Probably ratelimit, wait 2 minutes.",
|
message: `Failed to submit theme, ${res.message}`,
|
||||||
id: Toasts.genId(),
|
id: Toasts.genId(),
|
||||||
type: Toasts.Type.FAILURE,
|
type: Toasts.Type.FAILURE,
|
||||||
options: {
|
options: {
|
||||||
|
@ -531,8 +531,17 @@ function SubmitThemes() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(error => {
|
||||||
showToast("Failed to submit theme, try later", Toasts.Type.FAILURE);
|
logger.error("Failed to submit theme", error);
|
||||||
|
Toasts.show({
|
||||||
|
message: "Failed to submit theme, check your console!",
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
id: Toasts.genId(),
|
||||||
|
options: {
|
||||||
|
duration: 5e3,
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
size={Button.Sizes.MEDIUM}
|
size={Button.Sizes.MEDIUM}
|
||||||
|
@ -548,7 +557,7 @@ function SubmitThemes() {
|
||||||
marginTop: "8px",
|
marginTop: "8px",
|
||||||
marginLeft: "8px",
|
marginLeft: "8px",
|
||||||
}}>
|
}}>
|
||||||
By submitting your theme, you agree to your Discord User ID being processed.
|
Abusing this feature will result in you being blocked from further submissions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
|
|
|
@ -62,4 +62,15 @@
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
transition: fill 0.3s ease;
|
transition: fill 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vce-button-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vce-warning-button {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -4,30 +4,20 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import definePlugin from "@utils/types";
|
||||||
import { EquicordDevs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { SettingsRouter } from "@webpack/common";
|
import { SettingsRouter } from "@webpack/common";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
import { settings } from "./settings";
|
||||||
hideWarningCard: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
description: "Hide the warning card displayed at the top of the theme library tab",
|
|
||||||
restartNeeded: false,
|
|
||||||
},
|
|
||||||
domain: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
description: "Use Github instead of the default domain for themes",
|
|
||||||
restartNeeded: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ThemeLibrary",
|
name: "ThemeLibrary",
|
||||||
description: "A library of themes for Vencord.",
|
description: "A library of themes for Vencord.",
|
||||||
authors: [EquicordDevs.Fafa],
|
authors: [
|
||||||
|
{
|
||||||
|
name: "Fafa",
|
||||||
|
id: 428188716641812481n,
|
||||||
|
},
|
||||||
|
],
|
||||||
settings,
|
settings,
|
||||||
toolboxActions: {
|
toolboxActions: {
|
||||||
"Open Theme Library": () => {
|
"Open Theme Library": () => {
|
||||||
|
|
80
src/equicordplugins/themeLibrary/settings.tsx
Normal file
80
src/equicordplugins/themeLibrary/settings.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { OpenExternalIcon } from "@components/Icons";
|
||||||
|
import { OptionType } from "@utils/types";
|
||||||
|
import { Button, Clipboard, Forms, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
|
import { authorizeUser, deauthorizeUser } from "./auth";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vce-");
|
||||||
|
|
||||||
|
export const settings = definePluginSettings({
|
||||||
|
hideWarningCard: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
description: "Hide the warning card displayed at the top of the theme library tab",
|
||||||
|
restartNeeded: false,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
description: "ThemeLibrary Buttons",
|
||||||
|
component: () => {
|
||||||
|
const handleClick = async () => {
|
||||||
|
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
||||||
|
|
||||||
|
if (!token) return Toasts.show({
|
||||||
|
message: "No token to copy, try authorizing first!",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
options: {
|
||||||
|
duration: 2.5e3,
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipboard.copy(token);
|
||||||
|
|
||||||
|
Toasts.show({
|
||||||
|
message: "Copied to Clipboard!",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.SUCCESS,
|
||||||
|
options: {
|
||||||
|
duration: 2.5e3,
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle tag="h3" style={{ marginTop: 0, marginBottom: 8 }}>ThemeLibrary Auth</Forms.FormTitle>
|
||||||
|
<div className={cl("button-grid")}>
|
||||||
|
<Button onClick={() => authorizeUser()}>
|
||||||
|
Authorize with ThemeLibrary
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleClick}>
|
||||||
|
Copy ThemeLibrary Token
|
||||||
|
</Button>
|
||||||
|
<Button color={Button.Colors.RED} onClick={() => deauthorizeUser()}>
|
||||||
|
Deauthorize ThemeLibrary
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 8 }}>Theme Removal</Forms.FormTitle>
|
||||||
|
<Forms.FormText style={{ marginTop: 0, marginBottom: 8 }}> All Theme Authors are given credit in the theme info, no source has been modified, if you wish your theme to be removed anyway, open an Issue by clicking below.</Forms.FormText>
|
||||||
|
<div className={cl("button-grid")}>
|
||||||
|
<Button onClick={() => VencordNative.native.openExternal("https://github.com/Faf4a/plugins/issues/new?labels=removal&projects=&template=request_removal.yml&title=Theme+Removal")}>
|
||||||
|
Request Theme Removal <OpenExternalIcon height={16} width={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -65,7 +65,6 @@ export const enum SearchStatus {
|
||||||
export type ThemeLikeProps = {
|
export type ThemeLikeProps = {
|
||||||
status: number;
|
status: number;
|
||||||
likes: [{
|
likes: [{
|
||||||
_id?: string;
|
|
||||||
themeId: number;
|
themeId: number;
|
||||||
userIds: User["id"][];
|
userIds: User["id"][];
|
||||||
}];
|
}];
|
||||||
|
|
Loading…
Add table
Reference in a new issue