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:
Fafa 2025-01-03 04:32:13 +01:00 committed by GitHub
parent 59b76e8867
commit 5caa7c059f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 133 additions and 740 deletions

View file

@ -5,8 +5,7 @@
*/
import * as DataStore from "@api/DataStore";
import { Button, useEffect, useRef, UserStore, useState } from "@webpack/common";
import type { User } from "discord-types/general";
import { Button, useEffect, useRef, useState } from "@webpack/common";
import type { Theme, ThemeLikeProps } from "../types";
import { isAuthorized } from "../utils/auth";
@ -25,14 +24,13 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
function getThemeLikes(themeId: Theme["id"]): 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"]) => {
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) || themeId === "preview") ?? false;
const hasLiked: boolean = theme?.hasLiked ?? false;
const endpoint = hasLiked ? "/likes/remove" : "/likes/add";
const token = await DataStore.get("ThemeLibrary_uniqueToken");
@ -46,9 +44,9 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
token,
themeId: themeId,
}),
});
@ -57,7 +55,12 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
const fetchLikes = async () => {
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();
setLikedThemes(data);
} catch (err) {
@ -72,7 +75,7 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
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 (
<div>

View file

@ -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 handleAddRemoveTheme = () => {
const onlineThemeLinks = themeLinks.includes(`${apiUrl}/${theme.name}`)
? themeLinks.filter(link => link !== `${apiUrl}/${theme.name}`)
: [...themeLinks, `${apiUrl}/${theme.name}`];
const onlineThemeLinks = themeLinks.includes(`${apiUrl}/${theme.id}`)
? themeLinks.filter(link => link !== `${apiUrl}/${theme.id}`)
: [...themeLinks, `${apiUrl}/${theme.id}`];
setThemeLinks(onlineThemeLinks);
Vencord.Settings.themeLinks = onlineThemeLinks;
@ -108,7 +108,7 @@ export const ThemeCard: React.FC<ThemeCardProps> = ({ theme, themeLinks, likedTh
if (source) {
VencordNative.native.openExternal(source);
} 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>
)}
{!removeButtons && (
< div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
<Button
onClick={handleThemeAttributesCheck}
size={Button.Sizes.MEDIUM}
color={themeLinks.includes(`${apiUrl}/${theme.name}`) ? Button.Colors.RED : Button.Colors.GREEN}
color={themeLinks.includes(`${apiUrl}/${theme.id}`) ? Button.Colors.RED : Button.Colors.GREEN}
look={Button.Looks.FILLED}
className={Margins.right8}
disabled={!theme.content || theme.id === "preview"}
>
{themeLinks.includes(`${apiUrl}/${theme.name}`) ? "Remove Theme" : "Add Theme"}
{themeLinks.includes(`${apiUrl}/${theme.id}`) ? "Remove Theme" : "Add Theme"}
</Button>
<Button
onClick={async () => {

View file

@ -11,21 +11,18 @@ import { Settings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { OpenExternalIcon } from "@components/Icons";
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
import { fetchUserProfile } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
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 { isAuthorized } from "../utils/auth";
import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
import { ThemeCard } from "./ThemeCard";
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 async function fetchAllThemes(): Promise<Theme[]> {
@ -34,7 +31,7 @@ export async function fetchAllThemes(): Promise<Theme[]> {
const themes: Theme[] = Object.values(data);
themes.forEach(theme => {
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());
@ -71,6 +68,7 @@ function ThemeTab() {
const themeFilter = (theme: Theme) => {
const enabled = themeLinks.includes(`${apiUrl}/${theme.name}`);
const tags = new Set(theme.tags.map(tag => tag?.toLowerCase()));
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
@ -93,7 +91,12 @@ function ThemeTab() {
const fetchLikes = async () => {
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();
return data;
} catch (err) {
@ -260,641 +263,23 @@ function ThemeTab() {
// rework this!
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 (
<div className={classes(Margins.bottom8, Margins.top16)}>
<Forms.FormTitle tag="h2" style={{
overflowWrap: "break-word",
marginTop: 8,
marginBottom: 8,
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "70vh",
fontSize: "1.5em",
color: "var(--text-normal)"
}}>
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
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={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
opacity: 0,
cursor: "pointer",
}}
/>
{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 style={{
fontSize: "16px",
marginTop: "8px",
marginLeft: "8px",
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>
<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={{
fontSize: ".75em",
color: "var(--text-muted)"
}}> Thank you for your understanding!</p>
</div>
);
}

View file

@ -1,129 +1,129 @@
[data-tab-id="ThemeLibrary"]::before {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-mask: var(--si-widget) center/contain no-repeat !important;
mask: var(--si-widget) center/contain no-repeat !important;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-mask: var(--si-widget) center/contain no-repeat !important;
mask: var(--si-widget) center/contain no-repeat !important;
}
.vce-theme-info {
padding: 0.5rem;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
overflow: hidden;
transition: all 0.3s ease;
padding: 0.5rem;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
overflow: hidden;
transition: all 0.3s ease;
}
.vce-theme-info-preview {
max-width: 100%;
max-height: 100%;
border-radius: 5px;
margin: 0.5rem;
max-width: 100%;
max-height: 100%;
border-radius: 5px;
margin: 0.5rem;
}
.vce-theme-info-preview img {
width: 1080px !important;
height: 1920px !important;
object-fit: cover;
width: 1080px !important;
height: 1920px !important;
object-fit: cover;
}
.vce-theme-text {
padding: 0.5rem;
padding: 0.5rem;
}
.vce-theme-info-tag {
background: var(--background-secondary);
color: var(--text-primary);
border: 2px solid var(--background-tertiary);
padding: 0.25rem 0.5rem;
display: inline-block;
border-radius: 5px;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
background: var(--background-secondary);
color: var(--text-primary);
border: 2px solid var(--background-tertiary);
padding: 0.25rem 0.5rem;
display: inline-block;
border-radius: 5px;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.vce-text-input {
display: inline-block !important;
color: var(--text-normal) !important;
font-size: 16px !important;
padding: 0.5em;
border: 2px solid var(--background-tertiary);
line-height: 1.2;
max-height: unset;
display: inline-block !important;
color: var(--text-normal) !important;
font-size: 16px !important;
padding: 0.5em;
border: 2px solid var(--background-tertiary);
line-height: 1.2;
max-height: unset;
}
.vce-likes-icon {
overflow: visible;
margin-right: 0.5rem;
transition: fill 0.3s ease;
overflow: visible;
margin-right: 0.5rem;
transition: fill 0.3s ease;
}
.vce-button-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.vce-warning-button {
display: flex;
width: 100%;
display: flex;
width: 100%;
}
.vce-search-grid {
display: grid;
height: 40px;
gap: 10px;
grid-template-columns: 1fr 200px;
display: grid;
height: 40px;
gap: 10px;
grid-template-columns: 1fr 200px;
}
.vce-button {
white-space: normal;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: normal;
display: inline-flex;
align-items: center;
justify-content: center;
}
.vce-overwrite-modal {
border: 1px solid var(--background-modifier-accent);
border-radius: 8px;
padding: 0.5em;
border: 1px solid var(--background-modifier-accent);
border-radius: 8px;
padding: 0.5em;
}
.vce-image-paste {
border-radius: 8px;
border: 3px dashed var(--background-modifier-accent);
background-color: var(--background-secondary);
padding: 20px;
text-align: center;
position: relative;
cursor: pointer;
margin-top: 20px;
transition: border 0.3s ease;
border-radius: 8px;
border: 3px dashed var(--background-modifier-accent);
background-color: var(--background-secondary);
padding: 20px;
text-align: center;
position: relative;
cursor: pointer;
margin-top: 20px;
transition: border 0.3s ease;
}
.vce-image-paste:hover {
border: 3px dashed var(--brand-500);
transition: border 0.3s ease;
border: 3px dashed var(--brand-500);
transition: border 0.3s ease;
}
.vce-styled-list {
list-style-type: none;
padding: 0;
margin: 0;
list-style-type: none;
padding: 0;
margin: 0;
}
.vce-styled-list li {
padding: 10px 0;
border-bottom: thin solid var(--background-modifier-accent);
padding: 10px 0;
border-bottom: thin solid var(--background-modifier-accent);
}
.vce-styled-list li:last-child {
border-bottom: none;
border-bottom: none;
}
.vce-divider-border {
border-bottom: thin solid var(--background-modifier-accent);
padding: 10px 0;
width: 100%;
border-bottom: thin solid var(--background-modifier-accent);
padding: 10px 0;
width: 100%;
}

View file

@ -28,13 +28,15 @@ export default definePlugin({
}
).customSections;
customSettingsSections.push(_ => ({
const ThemeSection = () => ({
section: "ThemeLibrary",
label: "Theme Library",
searchableTitles: ["Theme Library"],
element: require("./components/ThemeTab").default,
id: "ThemeSection",
}));
});
customSettingsSections.push(ThemeSection);
},
stop() {

View file

@ -21,5 +21,7 @@ export function getThemesDir(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme)
export async function downloadTheme(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) {
if (!theme.content || !theme.name) return;
const path = join(dir.toString(), `${theme.name}.theme.css`);
writeFileSync(path, Buffer.from(theme.content, "base64"));
const download = await fetch(`https://discord-themes.com/api/download/${theme.id}`);
const content = await download.text();
writeFileSync(path, content);
}

View file

@ -65,7 +65,8 @@ export type ThemeLikeProps = {
status: number;
likes: [{
themeId: number;
userIds: User["id"][];
likes: number;
hasLiked?: boolean;
}];
};

View file

@ -18,9 +18,9 @@ export async function authorizeUser(triggerModal: boolean = true) {
if (!triggerModal) return false;
openModal((props: any) => <OAuth2AuthorizeModal
{...props}
scopes={["identify"]}
scopes={["identify", "connections"]}
responseType="code"
redirectUri="https://themes-delta.vercel.app/api/user/auth"
redirectUri="https://discord-themes.com/api/user/auth"
permissions={0n}
clientId="1257819493422465235"
cancelCompletesFlow={false}
@ -79,9 +79,10 @@ export async function deauthorizeUser() {
const res = await themeRequest("/user/revoke", {
method: "DELETE",
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) {
@ -112,11 +113,10 @@ export async function getAuthorization() {
} else {
// check if valid
const res = await themeRequest("/user/findUserByToken", {
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"Authorization": `Bearer ${uniqueToken}`
},
body: JSON.stringify({ token: uniqueToken })
});
if (res.status === 400 || res.status === 500) {