mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-20 15:18:50 -05: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 { 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>
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -65,7 +65,8 @@ export type ThemeLikeProps = {
|
|||
status: number;
|
||||
likes: [{
|
||||
themeId: number;
|
||||
userIds: User["id"][];
|
||||
likes: number;
|
||||
hasLiked?: boolean;
|
||||
}];
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue