mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-20 15:18:50 -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
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
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 { isAuthorized } from "../auth";
|
||||
import type { Theme, ThemeLikeProps } from "../types";
|
||||
import { themeRequest } from "./ThemeTab";
|
||||
|
||||
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 logger = new Logger("ThemeLibrary", "#e5c890");
|
||||
|
||||
export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { themeId: Theme["id"], likedThemes: ThemeLikeProps | undefined; }) => {
|
||||
const [likesCount, setLikesCount] = useState(0);
|
||||
const [likedThemes, setLikedThemes] = useState(initialLikedThemes);
|
||||
const debounce = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const likes = getThemeLikes(themeId);
|
||||
logger.debug("likes", likes, "for:", themeId);
|
||||
setLikesCount(likes);
|
||||
}, [likedThemes, themeId]);
|
||||
|
||||
|
@ -45,10 +37,17 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
|||
);
|
||||
|
||||
const handleLikeClick = async (themeId: Theme["id"]) => {
|
||||
if (!isAuthorized()) return;
|
||||
const theme = likedThemes?.likes.find(like => like.themeId === themeId as unknown as Number);
|
||||
const currentUser: User = UserStore.getCurrentUser();
|
||||
const hasLiked: boolean = theme?.userIds.includes(currentUser.id) ?? false;
|
||||
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 {
|
||||
const response = await themeRequest(endpoint, {
|
||||
|
@ -56,14 +55,13 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
|||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
body: JSON.stringify({
|
||||
userId: currentUser.id,
|
||||
token,
|
||||
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 () => {
|
||||
try {
|
||||
|
@ -76,11 +74,10 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
|||
};
|
||||
|
||||
fetchLikes();
|
||||
// doing it locally isnt the best way probably, but it does the same
|
||||
setLikesCount(likesCount + (hasLiked ? -1 : 1));
|
||||
} catch (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;
|
||||
|
|
|
@ -18,7 +18,7 @@ const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaul
|
|||
|
||||
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 donate = metadata.match(/@donate\s+(.+)/)?.[1] || "";
|
||||
const version = metadata.match(/@version\s+(.+)/)?.[1] || "";
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
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";
|
||||
|
@ -20,16 +21,17 @@ import { Margins } from "@utils/margins";
|
|||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
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 { Constructor } from "type-fest";
|
||||
|
||||
import { isAuthorized } from "../auth";
|
||||
import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
|
||||
import { LikesComponent } from "./LikesComponent";
|
||||
import { ThemeInfoModal } from "./ThemeInfoModal";
|
||||
|
||||
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 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");
|
||||
|
||||
async function fetchThemes(url: string): Promise<Theme[]> {
|
||||
const response = await fetch(url);
|
||||
export async function fetchAllThemes(): Promise<Theme[]> {
|
||||
const response = await themeRequest("/themes");
|
||||
const data = await response.json();
|
||||
const themes: Theme[] = Object.values(data);
|
||||
themes.forEach(theme => {
|
||||
if (!theme.source) {
|
||||
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());
|
||||
}
|
||||
|
||||
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 = {}) {
|
||||
return fetch(API_URL + path, {
|
||||
...options,
|
||||
|
@ -115,7 +102,7 @@ function ThemeTab() {
|
|||
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||
|
||||
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 (!theme.tags.includes("theme") && searchValue.status === SearchStatus.THEME) return false;
|
||||
if (!theme.tags.includes("snippet") && searchValue.status === SearchStatus.SNIPPET) return false;
|
||||
|
@ -146,7 +133,7 @@ function ThemeTab() {
|
|||
useEffect(() => {
|
||||
const fetchThemes = async () => {
|
||||
try {
|
||||
const themes = await API_TYPE({}, true);
|
||||
const themes = await fetchAllThemes();
|
||||
// fetch likes
|
||||
setThemes(themes);
|
||||
const likes = await fetchLikes();
|
||||
|
@ -187,7 +174,7 @@ function ThemeTab() {
|
|||
) : (
|
||||
<>
|
||||
{hideWarningCard ? null : (
|
||||
<ErrorCard id="vc-themetab-warning">
|
||||
<ErrorCard>
|
||||
<Forms.FormTitle tag="h4">Want your theme removed?</Forms.FormTitle>
|
||||
<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>
|
||||
|
@ -200,7 +187,7 @@ function ThemeTab() {
|
|||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.RED}
|
||||
look={Button.Looks.FILLED}
|
||||
className={Margins.top8}
|
||||
className={classes(Margins.top16, "vce-warning-button")}
|
||||
>Hide</Button>
|
||||
</ErrorCard >
|
||||
)}
|
||||
|
@ -247,10 +234,10 @@ function ThemeTab() {
|
|||
</Forms.FormText>
|
||||
)}
|
||||
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
||||
{themeLinks.includes(API_TYPE(theme)) ? (
|
||||
{themeLinks.includes(`${API_URL}/${theme.name}`) ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const onlineThemeLinks = themeLinks.filter(x => x !== API_TYPE(theme));
|
||||
const onlineThemeLinks = themeLinks.filter(x => x !== `${API_URL}/${theme.name}`);
|
||||
setThemeLinks(onlineThemeLinks);
|
||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||
}}
|
||||
|
@ -264,7 +251,7 @@ function ThemeTab() {
|
|||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const onlineThemeLinks = [...themeLinks, API_TYPE(theme)];
|
||||
const onlineThemeLinks = [...themeLinks, `${API_URL}/${theme.name}`];
|
||||
setThemeLinks(onlineThemeLinks);
|
||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||
}}
|
||||
|
@ -289,14 +276,14 @@ function ThemeTab() {
|
|||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const content = atob(theme.content);
|
||||
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(API_TYPE(theme).replace("?raw=true", ""));
|
||||
VencordNative.native.openExternal(`${API_URL}/${theme.name}`);
|
||||
}
|
||||
}}
|
||||
size={Button.Sizes.MEDIUM}
|
||||
|
@ -318,10 +305,10 @@ function ThemeTab() {
|
|||
}}>
|
||||
Themes
|
||||
</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} />
|
||||
<div className={InputStyles.inputWrapper}>
|
||||
<Select
|
||||
<SearchableSelect
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Themes", value: SearchStatus.THEME },
|
||||
|
@ -333,10 +320,12 @@ function ThemeTab() {
|
|||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||
]}
|
||||
serialize={String}
|
||||
select={onStatusChange}
|
||||
isSelected={v => v === searchValue.status}
|
||||
// @ts-ignore
|
||||
value={searchValue.status}
|
||||
clearable={false}
|
||||
onChange={v => onStatusChange(v as SearchStatus)}
|
||||
closeOnSelect={true}
|
||||
className={InputStyles.inputDefault}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -382,10 +371,10 @@ function ThemeTab() {
|
|||
</Forms.FormText>
|
||||
)}
|
||||
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
||||
{themeLinks.includes(API_TYPE(theme)) ? (
|
||||
{themeLinks.includes(`${API_URL}/${theme.name}`) ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const onlineThemeLinks = themeLinks.filter(x => x !== API_TYPE(theme));
|
||||
const onlineThemeLinks = themeLinks.filter(x => x !== `${API_URL}/${theme.name}`);
|
||||
setThemeLinks(onlineThemeLinks);
|
||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||
}}
|
||||
|
@ -399,7 +388,7 @@ function ThemeTab() {
|
|||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const onlineThemeLinks = [...themeLinks, API_TYPE(theme)];
|
||||
const onlineThemeLinks = [...themeLinks, `${API_URL}/${theme.name}`];
|
||||
setThemeLinks(onlineThemeLinks);
|
||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||
}}
|
||||
|
@ -425,14 +414,14 @@ function ThemeTab() {
|
|||
<LikesComponent themeId={theme.id} likedThemes={likedThemes} />
|
||||
<Button
|
||||
onClick={() => {
|
||||
const content = atob(theme.content);
|
||||
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(API_TYPE(theme).replace("?raw=true", ""));
|
||||
VencordNative.native.openExternal(`${API_URL}/${theme.name}`);
|
||||
}
|
||||
}}
|
||||
size={Button.Sizes.MEDIUM}
|
||||
|
@ -455,9 +444,7 @@ function ThemeTab() {
|
|||
}
|
||||
|
||||
function SubmitThemes() {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const [themeContent, setContent] = useState("");
|
||||
|
||||
const handleChange = (v: string) => setContent(v);
|
||||
|
||||
return (
|
||||
|
@ -483,7 +470,7 @@ function SubmitThemes() {
|
|||
}}>
|
||||
Submit Themes
|
||||
</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!
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
|
@ -497,8 +484,20 @@ function SubmitThemes() {
|
|||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (themeContent.length < 50) return showToast("Theme content is too short, must be above 50", Toasts.Type.FAILURE);
|
||||
onClick={async () => {
|
||||
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", {
|
||||
method: "POST",
|
||||
|
@ -506,13 +505,14 @@ function SubmitThemes() {
|
|||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: `${currentUser.id}`,
|
||||
content: btoa(themeContent),
|
||||
token,
|
||||
content: window.btoa(themeContent),
|
||||
}),
|
||||
}).then(response => {
|
||||
}).then(async response => {
|
||||
if (!response.ok) {
|
||||
const res = await response.json();
|
||||
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(),
|
||||
type: Toasts.Type.FAILURE,
|
||||
options: {
|
||||
|
@ -531,8 +531,17 @@ function SubmitThemes() {
|
|||
}
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
showToast("Failed to submit theme, try later", Toasts.Type.FAILURE);
|
||||
}).catch(error => {
|
||||
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}
|
||||
|
@ -548,7 +557,7 @@ function SubmitThemes() {
|
|||
marginTop: "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>
|
||||
</div>
|
||||
</Forms.FormText>
|
||||
|
|
|
@ -62,4 +62,15 @@
|
|||
overflow: visible;
|
||||
margin-right: 0.5rem;
|
||||
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
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import definePlugin from "@utils/types";
|
||||
import { SettingsRouter } from "@webpack/common";
|
||||
|
||||
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,
|
||||
},
|
||||
domain: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
description: "Use Github instead of the default domain for themes",
|
||||
restartNeeded: false,
|
||||
},
|
||||
});
|
||||
import { settings } from "./settings";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ThemeLibrary",
|
||||
description: "A library of themes for Vencord.",
|
||||
authors: [EquicordDevs.Fafa],
|
||||
authors: [
|
||||
{
|
||||
name: "Fafa",
|
||||
id: 428188716641812481n,
|
||||
},
|
||||
],
|
||||
settings,
|
||||
toolboxActions: {
|
||||
"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 = {
|
||||
status: number;
|
||||
likes: [{
|
||||
_id?: string;
|
||||
themeId: number;
|
||||
userIds: User["id"][];
|
||||
}];
|
||||
|
|
Loading…
Add table
Reference in a new issue