mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-16 09:57:08 -04:00
update theme library (#115)
* Merge branch 'dev' * update theme library * update author to use equicord constants * Delete Dev Stuff For Merge
This commit is contained in:
parent
59b76e8867
commit
5caa7c059f
8 changed files with 133 additions and 740 deletions
|
@ -5,8 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { Button, useEffect, useRef, UserStore, useState } from "@webpack/common";
|
import { Button, useEffect, useRef, useState } from "@webpack/common";
|
||||||
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";
|
||||||
|
@ -25,14 +24,13 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
||||||
|
|
||||||
function getThemeLikes(themeId: Theme["id"]): number {
|
function getThemeLikes(themeId: Theme["id"]): number {
|
||||||
const themeLike = likedThemes?.likes.find(like => like.themeId === themeId as unknown as Number);
|
const themeLike = likedThemes?.likes.find(like => like.themeId === themeId as unknown as Number);
|
||||||
return themeLike ? themeLike.userIds.length : 0;
|
return themeLike ? themeLike.likes : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
const currentUser: User = UserStore.getCurrentUser();
|
const hasLiked: boolean = theme?.hasLiked ?? 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");
|
||||||
|
|
||||||
|
@ -46,9 +44,9 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
token,
|
|
||||||
themeId: themeId,
|
themeId: themeId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -57,7 +55,12 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
||||||
|
|
||||||
const fetchLikes = async () => {
|
const fetchLikes = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await themeRequest("/likes/get");
|
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
||||||
|
const response = await themeRequest("/likes/get", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setLikedThemes(data);
|
setLikedThemes(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -72,7 +75,7 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
|
||||||
debounce.current = false;
|
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?.hasLiked === true) ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -49,9 +49,9 @@ export const ThemeCard: React.FC<ThemeCardProps> = ({ theme, themeLinks, likedTh
|
||||||
const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
|
const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
|
||||||
|
|
||||||
const handleAddRemoveTheme = () => {
|
const handleAddRemoveTheme = () => {
|
||||||
const onlineThemeLinks = themeLinks.includes(`${apiUrl}/${theme.name}`)
|
const onlineThemeLinks = themeLinks.includes(`${apiUrl}/${theme.id}`)
|
||||||
? themeLinks.filter(link => link !== `${apiUrl}/${theme.name}`)
|
? themeLinks.filter(link => link !== `${apiUrl}/${theme.id}`)
|
||||||
: [...themeLinks, `${apiUrl}/${theme.name}`];
|
: [...themeLinks, `${apiUrl}/${theme.id}`];
|
||||||
|
|
||||||
setThemeLinks(onlineThemeLinks);
|
setThemeLinks(onlineThemeLinks);
|
||||||
Vencord.Settings.themeLinks = onlineThemeLinks;
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
|
@ -108,7 +108,7 @@ export const ThemeCard: React.FC<ThemeCardProps> = ({ theme, themeLinks, likedTh
|
||||||
if (source) {
|
if (source) {
|
||||||
VencordNative.native.openExternal(source);
|
VencordNative.native.openExternal(source);
|
||||||
} else {
|
} else {
|
||||||
VencordNative.native.openExternal(`${apiUrl}/${theme.name}`);
|
VencordNative.native.openExternal(`${apiUrl}/${theme.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,16 +135,16 @@ export const ThemeCard: React.FC<ThemeCardProps> = ({ theme, themeLinks, likedTh
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
{!removeButtons && (
|
{!removeButtons && (
|
||||||
< div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleThemeAttributesCheck}
|
onClick={handleThemeAttributesCheck}
|
||||||
size={Button.Sizes.MEDIUM}
|
size={Button.Sizes.MEDIUM}
|
||||||
color={themeLinks.includes(`${apiUrl}/${theme.name}`) ? Button.Colors.RED : Button.Colors.GREEN}
|
color={themeLinks.includes(`${apiUrl}/${theme.id}`) ? Button.Colors.RED : Button.Colors.GREEN}
|
||||||
look={Button.Looks.FILLED}
|
look={Button.Looks.FILLED}
|
||||||
className={Margins.right8}
|
className={Margins.right8}
|
||||||
disabled={!theme.content || theme.id === "preview"}
|
disabled={!theme.content || theme.id === "preview"}
|
||||||
>
|
>
|
||||||
{themeLinks.includes(`${apiUrl}/${theme.name}`) ? "Remove Theme" : "Add Theme"}
|
{themeLinks.includes(`${apiUrl}/${theme.id}`) ? "Remove Theme" : "Add Theme"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|
|
@ -11,21 +11,18 @@ import { Settings } from "@api/Settings";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { OpenExternalIcon } from "@components/Icons";
|
import { OpenExternalIcon } from "@components/Icons";
|
||||||
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
|
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
|
||||||
import { fetchUserProfile } from "@utils/discord";
|
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Button, Forms, React, SearchableSelect, Switch, TabBar, Text, TextArea, TextInput, Toasts, useEffect, UserProfileStore, UserStore, useState, useStateFromStores } from "@webpack/common";
|
import { Button, Forms, React, SearchableSelect, TabBar, TextInput, useEffect, useState } from "@webpack/common";
|
||||||
|
|
||||||
import { Contributor, SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
|
import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
|
||||||
import { isAuthorized } from "../utils/auth";
|
|
||||||
import { ThemeCard } from "./ThemeCard";
|
import { ThemeCard } from "./ThemeCard";
|
||||||
|
|
||||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error");
|
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error");
|
||||||
|
|
||||||
export const apiUrl = "https://themes-delta.vercel.app/api";
|
export const apiUrl = "https://discord-themes.com/api";
|
||||||
export const logger = new Logger("ThemeLibrary", "#e5c890");
|
export const logger = new Logger("ThemeLibrary", "#e5c890");
|
||||||
|
|
||||||
export async function fetchAllThemes(): Promise<Theme[]> {
|
export async function fetchAllThemes(): Promise<Theme[]> {
|
||||||
|
@ -34,7 +31,7 @@ export async function fetchAllThemes(): Promise<Theme[]> {
|
||||||
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 = `${apiUrl}/${theme.name}`;
|
theme.source = `${apiUrl}/${theme.id}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
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());
|
||||||
|
@ -71,6 +68,7 @@ function ThemeTab() {
|
||||||
|
|
||||||
const themeFilter = (theme: Theme) => {
|
const themeFilter = (theme: Theme) => {
|
||||||
const enabled = themeLinks.includes(`${apiUrl}/${theme.name}`);
|
const enabled = themeLinks.includes(`${apiUrl}/${theme.name}`);
|
||||||
|
|
||||||
const tags = new Set(theme.tags.map(tag => tag?.toLowerCase()));
|
const tags = new Set(theme.tags.map(tag => tag?.toLowerCase()));
|
||||||
|
|
||||||
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
||||||
|
@ -93,7 +91,12 @@ function ThemeTab() {
|
||||||
|
|
||||||
const fetchLikes = async () => {
|
const fetchLikes = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await themeRequest("/likes/get");
|
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
||||||
|
const response = await themeRequest("/likes/get", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -260,641 +263,23 @@ function ThemeTab() {
|
||||||
|
|
||||||
// rework this!
|
// rework this!
|
||||||
function SubmitThemes() {
|
function SubmitThemes() {
|
||||||
const currentUser = UserStore.getCurrentUser();
|
|
||||||
const currentUserProfile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(currentUser.id));
|
|
||||||
|
|
||||||
if (!currentUserProfile && currentUser.id) fetchUserProfile(currentUser.id);
|
|
||||||
|
|
||||||
const [theme, setTheme] = useState({
|
|
||||||
title: "",
|
|
||||||
description: "",
|
|
||||||
content: "",
|
|
||||||
version: "",
|
|
||||||
type: "theme",
|
|
||||||
attribution: {
|
|
||||||
include_github: false,
|
|
||||||
sourceLink: "",
|
|
||||||
donationLink: "",
|
|
||||||
contributors: [{
|
|
||||||
username: currentUser.username,
|
|
||||||
id: currentUser.id,
|
|
||||||
avatar: currentUser.getAvatarURL(),
|
|
||||||
github_username: currentUserProfile.connectedAccounts.find(x => x.type === "github")?.name ?? null,
|
|
||||||
}],
|
|
||||||
// isAllowedToRedistribute: false,
|
|
||||||
},
|
|
||||||
screenshotMetadata: {
|
|
||||||
data: "",
|
|
||||||
name: "",
|
|
||||||
size: 0,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const [valid, setValid] = useState(false);
|
|
||||||
|
|
||||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!e.target.files) return;
|
|
||||||
const image = e.target.files[0];
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const imgElement = new Image();
|
|
||||||
imgElement.src = reader.result as string;
|
|
||||||
|
|
||||||
imgElement.onload = () => {
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const maxWidth = 800;
|
|
||||||
const scaleSize = maxWidth / imgElement.width;
|
|
||||||
|
|
||||||
canvas.width = maxWidth;
|
|
||||||
canvas.height = imgElement.height * scaleSize;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
const resizedBase64String = canvas.toDataURL("image/jpeg", 0.7);
|
|
||||||
|
|
||||||
handleChange("screenshotMetadata", {
|
|
||||||
data: resizedBase64String,
|
|
||||||
name: image.name,
|
|
||||||
size: image.size,
|
|
||||||
});
|
|
||||||
setValid(!!theme.title && theme.content.length >= 50 && !!theme.version && !!theme.description);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (p, v) => {
|
|
||||||
setTheme(prevTheme => {
|
|
||||||
const [first, ...rest] = p.split(".");
|
|
||||||
|
|
||||||
const updateNestedObject = (obj, keys, value) => {
|
|
||||||
const key = keys[0];
|
|
||||||
if (keys.length === 1) {
|
|
||||||
return { ...obj, [key]: value };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...obj,
|
|
||||||
[key]: updateNestedObject(obj[key] || {}, keys.slice(1), value)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return updateNestedObject(prevTheme, [first, ...rest], v);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const setContributors = contributors => {
|
|
||||||
setTheme(prevTheme => ({
|
|
||||||
...prevTheme,
|
|
||||||
attribution: {
|
|
||||||
...prevTheme.attribution,
|
|
||||||
contributors: [prevTheme.attribution.contributors[0], ...contributors]
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
logger.debug(valid);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes(Margins.bottom8, Margins.top16)}>
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
Submission Guidelines
|
|
||||||
</Forms.FormTitle>
|
|
||||||
<Text>
|
|
||||||
<ul className="vce-styled-list">
|
|
||||||
<li>Do not distribute themes or snippets that aren't yours.</li>
|
|
||||||
<li>Your submission must be at least 50 characters long.</li>
|
|
||||||
<li>Do not submit low-quality themes or snippets.</li>
|
|
||||||
</ul>
|
|
||||||
<Text color="text-muted" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 8,
|
|
||||||
}} variant="heading-sm/medium">
|
|
||||||
Fields with <span style={{ color: "var(--status-danger)" }}>*</span> are required!
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", alignItems: "center", marginTop: 16, marginBottom: 16 }}>
|
|
||||||
<div style={{ flex: 5, marginRight: 8 }}>
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
{theme.type.charAt(0).toUpperCase()}{theme.type.slice(1)} Name <span style={{ color: "var(--status-danger)" }}>*</span>
|
|
||||||
</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
onChange={e => { }}
|
|
||||||
onBlur={e => {
|
|
||||||
const v = e.target.value;
|
|
||||||
handleChange("title", v);
|
|
||||||
setValid(!!theme.title && !!v && theme.content.length >= 50 && !!theme.version && !!theme.description && !!theme.screenshotMetadata.data);
|
|
||||||
}}
|
|
||||||
placeholder={`My awesome ${theme.type}`}
|
|
||||||
rows={1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 2 }}>
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
{theme.type.charAt(0).toUpperCase()}{theme.type.slice(1)} Version <span style={{ color: "var(--status-danger)" }}>*</span>
|
|
||||||
</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
onChange={e => { }}
|
|
||||||
onBlur={e => {
|
|
||||||
const v = e.target.value;
|
|
||||||
handleChange("version", v);
|
|
||||||
setValid(!!theme.title && theme.content.length >= 50 && !!v && !!theme.description && !!theme.screenshotMetadata.data);
|
|
||||||
}}
|
|
||||||
placeholder="v1.0.0"
|
|
||||||
rows={1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
{theme.type.charAt(0).toUpperCase()}{theme.type.slice(1)} Description <span style={{ color: "var(--status-danger)" }}>*</span></Forms.FormTitle>
|
|
||||||
<Text color="text-muted" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginBottom: 8,
|
|
||||||
}} variant="heading-sm/medium">
|
|
||||||
Try to keep your description short and to the point.
|
|
||||||
</Text>
|
|
||||||
<TextArea
|
|
||||||
onChange={e => { }}
|
|
||||||
onBlur={e => {
|
|
||||||
const v = e.target.value;
|
|
||||||
handleChange("description", v);
|
|
||||||
if (v.length < 10) return Toasts.show({
|
|
||||||
message: `${theme.type.charAt(0).toUpperCase()}${theme.type.slice(1)} description must be at least 10 characters long!`,
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
options: {
|
|
||||||
duration: 2e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setValid(!!theme.title && v.length >= 10 && theme.content.length >= 50 && !!theme.version && !!theme.screenshotMetadata.data);
|
|
||||||
}}
|
|
||||||
placeholder={`My ${theme.type}..`}
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<Switch
|
|
||||||
value={theme.type === "snippet"}
|
|
||||||
onChange={(e: boolean) => {
|
|
||||||
handleChange("type", e ? "snippet" : "theme");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Snippet
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
{theme.type.charAt(0).toUpperCase()}{theme.type.slice(1)} Content <span style={{ color: "var(--status-danger)" }}>*</span></Forms.FormTitle>
|
|
||||||
<TextArea
|
|
||||||
onChange={e => { }}
|
|
||||||
onBlur={e => {
|
|
||||||
const v = e.target.value;
|
|
||||||
handleChange("content", v);
|
|
||||||
if (v.length < 50) return Toasts.show({
|
|
||||||
message: "Theme content must be at least 50 characters long!",
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
options: {
|
|
||||||
duration: 2e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setValid(!!theme.title && v.length >= 50 && !!theme.description && !!theme.version && !!theme.screenshotMetadata.data);
|
|
||||||
}}
|
|
||||||
placeholder="Your CSS here.."
|
|
||||||
className={"vce-text-input"}
|
|
||||||
rows={8}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h1" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 16,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>Attribution</Forms.FormTitle>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>Contributors</Forms.FormTitle>
|
|
||||||
<Text color="text-muted" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginBottom: 8,
|
|
||||||
}} variant="heading-sm/medium">
|
|
||||||
Contributors are people that contributed to your {theme.type}, they will be displayed on the {theme.type} card.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
|
|
||||||
<Forms.FormText>
|
|
||||||
{theme.attribution.contributors && theme.attribution.contributors.map(contributor => (
|
|
||||||
<div key={contributor.id} style={{ display: "flex", alignItems: "center", marginBottom: "8px" }}>
|
|
||||||
<img src={contributor.avatar} style={{ width: "32px", height: "32px", borderRadius: "50%", marginRight: "8px" }} />
|
|
||||||
<div>
|
|
||||||
<div>{contributor.username === currentUser.username ? contributor.username + " (you)" : contributor.username}</div>
|
|
||||||
<div style={{
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
}}>{contributor.id}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Forms.FormText>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
marginTop: "12px",
|
|
||||||
marginBottom: "12px"
|
|
||||||
}}>
|
|
||||||
<Switch
|
|
||||||
value={theme.attribution.include_github}
|
|
||||||
disabled={!theme.attribution.contributors.find(x => x.github_username)}
|
|
||||||
onChange={(e: boolean) => {
|
|
||||||
handleChange("attribution.include_github", e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Include the GitHub usernames of contributors{" "}
|
|
||||||
<span style={{
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
fontSize: "14px",
|
|
||||||
}}>
|
|
||||||
(this will be automatically fetched from their profile connections)
|
|
||||||
</span>.
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
<TextArea
|
|
||||||
onBlur={e => {
|
|
||||||
const users = e.target.value.trim()
|
|
||||||
.split(",").map(s => s.trim());
|
|
||||||
|
|
||||||
let valid = true;
|
|
||||||
let newContributors: Contributor[] = [];
|
|
||||||
|
|
||||||
for (const contributor of users) {
|
|
||||||
const user = UserStore.getUser(contributor);
|
|
||||||
if (!user) {
|
|
||||||
Toasts.show({
|
|
||||||
message: `User '${contributor}' doesn't exist`,
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
options: {
|
|
||||||
duration: 2e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
valid = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = UserProfileStore.getUserProfile(user.id);
|
|
||||||
if (!profile && user.id) fetchUserProfile(user.id);
|
|
||||||
|
|
||||||
newContributors.push({
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
avatar: user.getAvatarURL(),
|
|
||||||
github_username: profile.connectedAccounts.find(x => x.type === "github")?.name ?? null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove dups
|
|
||||||
newContributors = newContributors.filter((contributor, i, self) => i === self.findIndex(x => x.id === contributor.id));
|
|
||||||
|
|
||||||
setContributors(newContributors);
|
|
||||||
setValid(!!theme.title && valid && theme.content.length >= 50 && !!theme.version && !!theme.screenshotMetadata.data);
|
|
||||||
}}
|
|
||||||
onChange={v => { }}
|
|
||||||
placeholder="123456789012345, 234567891012345"
|
|
||||||
rows={1}
|
|
||||||
style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 16,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 16,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
Source </Forms.FormTitle>
|
|
||||||
<Text color="text-muted" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginBottom: 8,
|
|
||||||
}} variant="heading-sm/medium">
|
|
||||||
Please limit yourself to trusted sites like GitHub.
|
|
||||||
</Text>
|
|
||||||
<TextArea
|
|
||||||
onChange={e => { }}
|
|
||||||
onBlur={e => {
|
|
||||||
const v = e.target.value;
|
|
||||||
if (!v.startsWith("https://") && v !== "") {
|
|
||||||
setValid(false);
|
|
||||||
return Toasts.show({
|
|
||||||
message: "Source link must be a valid URL!",
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
options: {
|
|
||||||
duration: 2e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setValid(!!theme.title && theme.description.length >= 10 && theme.content.length >= 50 && !!theme.version && !!theme.screenshotMetadata.data);
|
|
||||||
handleChange("attribution.sourceLink", v);
|
|
||||||
}}
|
|
||||||
placeholder="https://github.com/..."
|
|
||||||
rows={1}
|
|
||||||
/>
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 16,
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
Donation Link</Forms.FormTitle>
|
|
||||||
<Text color="text-muted" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginBottom: 8,
|
|
||||||
}} variant="heading-sm/medium">
|
|
||||||
Please limit yourself to trusted sites.
|
|
||||||
</Text>
|
|
||||||
<TextArea
|
|
||||||
onChange={e => { }}
|
|
||||||
onBlur={e => {
|
|
||||||
const v = e.target.value;
|
|
||||||
if (!v.startsWith("https://") && v !== "") {
|
|
||||||
setValid(false);
|
|
||||||
return Toasts.show({
|
|
||||||
message: "Donation link must be a valid URL!",
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
options: {
|
|
||||||
duration: 2e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setValid(!!theme.title && theme.description.length >= 10 && theme.content.length >= 50 && !!theme.version && !!theme.screenshotMetadata.data);
|
|
||||||
handleChange("attribution.donationLink", v);
|
|
||||||
}}
|
|
||||||
|
|
||||||
placeholder="https://github.com/..."
|
|
||||||
rows={1}
|
|
||||||
/>
|
|
||||||
<div className="vce-divider-border" />
|
|
||||||
<Forms.FormTitle tag="h2" style={{
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
marginTop: 16,
|
|
||||||
}}>
|
|
||||||
Theme Preview <span style={{ color: "var(--status-danger)" }}>*</span>
|
|
||||||
</Forms.FormTitle>
|
|
||||||
<div
|
<div
|
||||||
className="vce-image-paste"
|
|
||||||
>
|
|
||||||
<Text color="header-primary" variant="heading-md/semibold">
|
|
||||||
Click to select a file!
|
|
||||||
</Text>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleImageChange}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
display: "flex",
|
||||||
top: 0,
|
flexDirection: "column",
|
||||||
left: 0,
|
justifyContent: "center",
|
||||||
width: "100%",
|
alignItems: "center",
|
||||||
height: "100%",
|
height: "70vh",
|
||||||
opacity: 0,
|
fontSize: "1.5em",
|
||||||
cursor: "pointer",
|
color: "var(--text-normal)"
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{theme.screenshotMetadata.data && (
|
|
||||||
<div style={{ marginTop: "20px" }}>
|
|
||||||
<img src={theme.screenshotMetadata.data} style={{ maxWidth: "100%", borderRadius: "10px" }} />
|
|
||||||
<Text color="text-muted" variant="heading-sm/medium">
|
|
||||||
{theme.screenshotMetadata.name} ({(theme.screenshotMetadata.size! / 1024).toFixed(2)} KB)
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="vce-divider-border" />
|
|
||||||
<div style={{
|
|
||||||
marginTop: "16px"
|
|
||||||
}}>
|
}}>
|
||||||
{(!theme.attribution.donationLink && valid) && (
|
<p> This tab was replaced in favour of the new website: </p>
|
||||||
|
<p><a href="https://discord-themes.com">discord-themes.com</a></p>
|
||||||
<p style={{
|
<p style={{
|
||||||
fontSize: "16px",
|
fontSize: ".75em",
|
||||||
marginTop: "8px",
|
color: "var(--text-muted)"
|
||||||
marginLeft: "8px",
|
}}> Thank you for your understanding!</p>
|
||||||
color: "var(--status-danger)"
|
|
||||||
}}>
|
|
||||||
You do not have a <b>donation link</b> set. Consider adding one.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{(!theme.attribution.sourceLink && valid) && (
|
|
||||||
<p style={{
|
|
||||||
fontSize: "16px",
|
|
||||||
marginTop: "8px",
|
|
||||||
marginBottom: "8px",
|
|
||||||
marginLeft: "8px",
|
|
||||||
color: "var(--status-danger)"
|
|
||||||
}}>
|
|
||||||
You do not have a <b>source link</b> set. Consider adding one, even if it's just your github profile!
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Forms.FormText>
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await isAuthorized();
|
|
||||||
|
|
||||||
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
|
||||||
|
|
||||||
if (!token) return;
|
|
||||||
|
|
||||||
if (!theme.attribution.include_github) {
|
|
||||||
// @ts-ignore -- too lazy to type this
|
|
||||||
theme.attribution.contributors = theme.attribution.contributors.map(contributor => {
|
|
||||||
const { github_username, ...rest } = contributor;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await themeRequest("/submit/theme", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
token,
|
|
||||||
title: theme.title,
|
|
||||||
type: theme.type,
|
|
||||||
description: theme.description,
|
|
||||||
version: theme.description,
|
|
||||||
content: theme.content,
|
|
||||||
attribution: theme.attribution,
|
|
||||||
screenshotMetadata: theme.screenshotMetadata
|
|
||||||
})
|
|
||||||
}).then(async response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
const res = await response.json();
|
|
||||||
logger.debug(theme);
|
|
||||||
return Toasts.show({
|
|
||||||
message: res.message,
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
options: {
|
|
||||||
duration: 5e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Toasts.show({
|
|
||||||
message: "Theme submitted successfully!",
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.SUCCESS,
|
|
||||||
options: {
|
|
||||||
duration: 3.5e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setTheme({
|
|
||||||
title: "",
|
|
||||||
description: "",
|
|
||||||
content: "",
|
|
||||||
version: "",
|
|
||||||
type: "theme",
|
|
||||||
attribution: {
|
|
||||||
sourceLink: "",
|
|
||||||
donationLink: "",
|
|
||||||
include_github: false,
|
|
||||||
contributors: [{
|
|
||||||
username: currentUser.username,
|
|
||||||
id: currentUser.id,
|
|
||||||
avatar: currentUser.getAvatarURL(),
|
|
||||||
github_username: currentUserProfile.connectedAccounts.find(x => x.type === "github")?.name ?? null,
|
|
||||||
}],
|
|
||||||
// isAllowedToRedistribute: false,
|
|
||||||
},
|
|
||||||
screenshotMetadata: {
|
|
||||||
data: "",
|
|
||||||
name: "",
|
|
||||||
size: 0,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
return Toasts.show({
|
|
||||||
message: err.message,
|
|
||||||
id: Toasts.genId(),
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
options: {
|
|
||||||
duration: 5e3,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
logger.debug(theme);
|
|
||||||
}}
|
|
||||||
size={Button.Sizes.MEDIUM}
|
|
||||||
color={Button.Colors.GREEN}
|
|
||||||
look={Button.Looks.FILLED}
|
|
||||||
className={Margins.top16}
|
|
||||||
disabled={!valid}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size={Button.Sizes.MEDIUM}
|
|
||||||
color={Button.Colors.BRAND}
|
|
||||||
look={Button.Looks.FILLED}
|
|
||||||
className={Margins.top16}
|
|
||||||
style={{
|
|
||||||
marginLeft: "8px"
|
|
||||||
}}
|
|
||||||
disabled={!valid}
|
|
||||||
onClick={() => {
|
|
||||||
openModal(props => (
|
|
||||||
<ModalRoot {...props} size={ModalSize.LARGE}>
|
|
||||||
<ModalHeader>
|
|
||||||
<Forms.FormTitle tag="h4">Preview</Forms.FormTitle>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalContent>
|
|
||||||
<ThemeCard
|
|
||||||
theme={{
|
|
||||||
id: "preview",
|
|
||||||
name: theme.title,
|
|
||||||
type: theme.type,
|
|
||||||
release_date: new Date(),
|
|
||||||
description: theme.description,
|
|
||||||
content: "",
|
|
||||||
version: theme.version,
|
|
||||||
author: theme.attribution.contributors.map(x => {
|
|
||||||
return {
|
|
||||||
discord_name: x.username,
|
|
||||||
discord_snowflake: x.id,
|
|
||||||
github_name: undefined,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
tags: [theme.type],
|
|
||||||
likes: 124,
|
|
||||||
thumbnail_url: theme.screenshotMetadata.data,
|
|
||||||
}}
|
|
||||||
themeLinks={[]}
|
|
||||||
// @ts-ignore
|
|
||||||
setLikedThemes={() => { }}
|
|
||||||
removeButtons={false}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onClick={props.onClose} size={Button.Sizes.MEDIUM} color={Button.Colors.RED} look={Button.Looks.FILLED}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalRoot>
|
|
||||||
));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Preview
|
|
||||||
</Button>
|
|
||||||
<p style={{
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
fontSize: "12px",
|
|
||||||
marginTop: "8px",
|
|
||||||
marginLeft: "12px",
|
|
||||||
}}>
|
|
||||||
Abusing this feature will result in you being blocked from further submissions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Forms.FormText>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,13 +28,15 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
).customSections;
|
).customSections;
|
||||||
|
|
||||||
customSettingsSections.push(_ => ({
|
const ThemeSection = () => ({
|
||||||
section: "ThemeLibrary",
|
section: "ThemeLibrary",
|
||||||
label: "Theme Library",
|
label: "Theme Library",
|
||||||
searchableTitles: ["Theme Library"],
|
searchableTitles: ["Theme Library"],
|
||||||
element: require("./components/ThemeTab").default,
|
element: require("./components/ThemeTab").default,
|
||||||
id: "ThemeSection",
|
id: "ThemeSection",
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
customSettingsSections.push(ThemeSection);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
|
|
@ -21,5 +21,7 @@ export function getThemesDir(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme)
|
||||||
export async function downloadTheme(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) {
|
export async function downloadTheme(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) {
|
||||||
if (!theme.content || !theme.name) return;
|
if (!theme.content || !theme.name) return;
|
||||||
const path = join(dir.toString(), `${theme.name}.theme.css`);
|
const path = join(dir.toString(), `${theme.name}.theme.css`);
|
||||||
writeFileSync(path, Buffer.from(theme.content, "base64"));
|
const download = await fetch(`https://discord-themes.com/api/download/${theme.id}`);
|
||||||
|
const content = await download.text();
|
||||||
|
writeFileSync(path, content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,8 @@ export type ThemeLikeProps = {
|
||||||
status: number;
|
status: number;
|
||||||
likes: [{
|
likes: [{
|
||||||
themeId: number;
|
themeId: number;
|
||||||
userIds: User["id"][];
|
likes: number;
|
||||||
|
hasLiked?: boolean;
|
||||||
}];
|
}];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,9 @@ export async function authorizeUser(triggerModal: boolean = true) {
|
||||||
if (!triggerModal) return false;
|
if (!triggerModal) return false;
|
||||||
openModal((props: any) => <OAuth2AuthorizeModal
|
openModal((props: any) => <OAuth2AuthorizeModal
|
||||||
{...props}
|
{...props}
|
||||||
scopes={["identify"]}
|
scopes={["identify", "connections"]}
|
||||||
responseType="code"
|
responseType="code"
|
||||||
redirectUri="https://themes-delta.vercel.app/api/user/auth"
|
redirectUri="https://discord-themes.com/api/user/auth"
|
||||||
permissions={0n}
|
permissions={0n}
|
||||||
clientId="1257819493422465235"
|
clientId="1257819493422465235"
|
||||||
cancelCompletesFlow={false}
|
cancelCompletesFlow={false}
|
||||||
|
@ -79,9 +79,10 @@ export async function deauthorizeUser() {
|
||||||
const res = await themeRequest("/user/revoke", {
|
const res = await themeRequest("/user/revoke", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${uniqueToken}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ token: uniqueToken, userId: UserStore.getCurrentUser().id })
|
body: JSON.stringify({ userId: UserStore.getCurrentUser().id })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -112,11 +113,10 @@ export async function getAuthorization() {
|
||||||
} else {
|
} else {
|
||||||
// check if valid
|
// check if valid
|
||||||
const res = await themeRequest("/user/findUserByToken", {
|
const res = await themeRequest("/user/findUserByToken", {
|
||||||
method: "POST",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${uniqueToken}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ token: uniqueToken })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 400 || res.status === 500) {
|
if (res.status === 400 || res.status === 500) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue