2024-05-04 00:50:59 -04:00
|
|
|
/*
|
|
|
|
|
* Vencord, a Discord client mod
|
|
|
|
|
* Copyright (c) 2024 Vendicated and contributors
|
|
|
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import "./styles.css";
|
|
|
|
|
|
2024-07-07 00:38:05 -04:00
|
|
|
import * as DataStore from "@api/DataStore";
|
2024-05-12 17:43:11 -04:00
|
|
|
import { Settings } from "@api/Settings";
|
2024-06-01 14:32:22 -04:00
|
|
|
import { ErrorCard } from "@components/ErrorCard";
|
2024-05-12 17:43:11 -04:00
|
|
|
import { OpenExternalIcon } from "@components/Icons";
|
2024-05-04 00:50:59 -04:00
|
|
|
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
|
2024-10-10 17:46:15 -04:00
|
|
|
import { fetchUserProfile } from "@utils/discord";
|
2024-06-24 14:53:58 -04:00
|
|
|
import { Logger } from "@utils/Logger";
|
2024-05-04 00:50:59 -04:00
|
|
|
import { Margins } from "@utils/margins";
|
|
|
|
|
import { classes } from "@utils/misc";
|
2024-08-29 08:15:50 -04:00
|
|
|
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
2024-08-16 21:47:39 -06:00
|
|
|
import { findByPropsLazy } from "@webpack";
|
2024-10-10 17:46:15 -04:00
|
|
|
import { Button, Forms, React, SearchableSelect, Switch, TabBar, Text, TextArea, TextInput, Toasts, useEffect, UserProfileStore, UserStore, useState, useStateFromStores } from "@webpack/common";
|
2024-05-04 00:50:59 -04:00
|
|
|
|
2024-10-10 17:46:15 -04:00
|
|
|
import { Contributor, SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
|
2024-07-12 11:31:17 -04:00
|
|
|
import { isAuthorized } from "../utils/auth";
|
2024-10-10 17:46:15 -04:00
|
|
|
import { ThemeCard } from "./ThemeCard";
|
2024-05-04 00:50:59 -04:00
|
|
|
|
2024-07-07 00:38:05 -04:00
|
|
|
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error");
|
2024-05-04 00:50:59 -04:00
|
|
|
|
2024-10-10 17:46:15 -04:00
|
|
|
export const apiUrl = "https://themes-delta.vercel.app/api";
|
|
|
|
|
export const logger = new Logger("ThemeLibrary", "#e5c890");
|
2024-06-24 14:53:58 -04:00
|
|
|
|
2024-07-07 00:38:05 -04:00
|
|
|
export async function fetchAllThemes(): Promise<Theme[]> {
|
|
|
|
|
const response = await themeRequest("/themes");
|
2024-05-12 17:43:11 -04:00
|
|
|
const data = await response.json();
|
|
|
|
|
const themes: Theme[] = Object.values(data);
|
|
|
|
|
themes.forEach(theme => {
|
|
|
|
|
if (!theme.source) {
|
2024-10-10 17:46:15 -04:00
|
|
|
theme.source = `${apiUrl}/${theme.name}`;
|
2024-05-12 17:43:11 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime());
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-24 14:53:58 -04:00
|
|
|
export async function themeRequest(path: string, options: RequestInit = {}) {
|
2024-10-10 17:46:15 -04:00
|
|
|
return fetch(apiUrl + path, {
|
2024-05-04 00:50:59 -04:00
|
|
|
...options,
|
|
|
|
|
headers: {
|
|
|
|
|
...options.headers,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-12 11:31:17 -04:00
|
|
|
const SearchTags = {
|
|
|
|
|
[SearchStatus.THEME]: "THEME",
|
|
|
|
|
[SearchStatus.SNIPPET]: "SNIPPET",
|
|
|
|
|
[SearchStatus.LIKED]: "LIKED",
|
|
|
|
|
[SearchStatus.DARK]: "DARK",
|
|
|
|
|
[SearchStatus.LIGHT]: "LIGHT",
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-04 00:50:59 -04:00
|
|
|
function ThemeTab() {
|
|
|
|
|
const [themes, setThemes] = useState<Theme[]>([]);
|
|
|
|
|
const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]);
|
|
|
|
|
const [themeLinks, setThemeLinks] = useState(Vencord.Settings.themeLinks);
|
2024-06-24 14:53:58 -04:00
|
|
|
const [likedThemes, setLikedThemes] = useState<ThemeLikeProps>();
|
2024-05-04 00:50:59 -04:00
|
|
|
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
|
2024-06-24 14:53:58 -04:00
|
|
|
const [hideWarningCard, setHideWarningCard] = useState(Settings.plugins.ThemeLibrary.hideWarningCard);
|
2024-05-04 00:50:59 -04:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
|
|
|
|
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
|
|
|
|
|
|
|
|
|
const themeFilter = (theme: Theme) => {
|
2024-10-10 17:46:15 -04:00
|
|
|
const enabled = themeLinks.includes(`${apiUrl}/${theme.name}`);
|
2024-08-29 08:15:50 -04:00
|
|
|
const tags = new Set(theme.tags.map(tag => tag?.toLowerCase()));
|
2024-07-12 11:31:17 -04:00
|
|
|
|
2024-05-04 00:50:59 -04:00
|
|
|
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
2024-07-12 11:31:17 -04:00
|
|
|
|
|
|
|
|
const anyTags = SearchTags[searchValue.status];
|
2024-08-29 08:15:50 -04:00
|
|
|
if (anyTags && !tags.has(anyTags?.toLowerCase())) return false;
|
2024-07-12 11:31:17 -04:00
|
|
|
|
|
|
|
|
if ((enabled && searchValue.status === SearchStatus.DISABLED) || (!enabled && searchValue.status === SearchStatus.ENABLED)) return false;
|
|
|
|
|
|
2024-05-04 00:50:59 -04:00
|
|
|
if (!searchValue.value.length) return true;
|
|
|
|
|
|
2024-08-29 08:15:50 -04:00
|
|
|
const v = searchValue.value?.toLowerCase();
|
2024-05-04 00:50:59 -04:00
|
|
|
return (
|
2024-08-29 08:15:50 -04:00
|
|
|
theme.name?.toLowerCase().includes(v) ||
|
|
|
|
|
theme.description?.toLowerCase().includes(v) ||
|
|
|
|
|
(Array.isArray(theme.author) ? theme.author.some(author => author.discord_name?.toLowerCase()?.includes(v)) : theme.author.discord_name?.toLowerCase()?.includes(v)) ||
|
2024-07-12 11:31:17 -04:00
|
|
|
tags.has(v)
|
2024-05-04 00:50:59 -04:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-24 14:53:58 -04:00
|
|
|
const fetchLikes = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await themeRequest("/likes/get");
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
return data;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error(err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-04 00:50:59 -04:00
|
|
|
useEffect(() => {
|
2024-07-12 11:31:17 -04:00
|
|
|
const fetchData = async () => {
|
2024-06-24 14:53:58 -04:00
|
|
|
try {
|
2024-07-12 11:31:17 -04:00
|
|
|
const [themes, likes] = await Promise.all([fetchAllThemes(), fetchLikes()]);
|
2024-06-24 14:53:58 -04:00
|
|
|
setThemes(themes);
|
|
|
|
|
setLikedThemes(likes);
|
|
|
|
|
setFilteredThemes(themes);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error(err);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-07-12 11:31:17 -04:00
|
|
|
fetchData();
|
2024-05-04 00:50:59 -04:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setThemeLinks(Vencord.Settings.themeLinks);
|
|
|
|
|
}, [Vencord.Settings.themeLinks]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-07-12 11:31:17 -04:00
|
|
|
// likes only update after 12_000 due to cache
|
|
|
|
|
if (searchValue.status === SearchStatus.LIKED) {
|
|
|
|
|
const likedThemes = themes.sort((a, b) => b.likes - a.likes);
|
|
|
|
|
// replacement of themeFilter which wont work with SearchStatus.LIKED
|
|
|
|
|
const filteredLikedThemes = likedThemes.filter(x => x.name.includes(searchValue.value));
|
|
|
|
|
setFilteredThemes(filteredLikedThemes);
|
|
|
|
|
} else {
|
|
|
|
|
const sortedThemes = themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime());
|
|
|
|
|
const filteredThemes = sortedThemes.filter(themeFilter);
|
|
|
|
|
setFilteredThemes(filteredThemes);
|
|
|
|
|
}
|
|
|
|
|
}, [searchValue, themes]);
|
2024-05-04 00:50:59 -04:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<>
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
2024-10-10 17:46:15 -04:00
|
|
|
flexDirection: "column",
|
2024-05-04 00:50:59 -04:00
|
|
|
justifyContent: "center",
|
|
|
|
|
alignItems: "center",
|
2024-10-10 17:46:15 -04:00
|
|
|
height: "70vh",
|
2024-05-04 00:50:59 -04:00
|
|
|
fontSize: "1.5em",
|
2024-10-10 17:46:15 -04:00
|
|
|
color: "var(--text-normal)"
|
|
|
|
|
}}>
|
|
|
|
|
<p> Getting the latest themes... </p>
|
|
|
|
|
<p style={{
|
|
|
|
|
fontSize: ".75em",
|
2024-05-04 00:50:59 -04:00
|
|
|
color: "var(--text-muted)"
|
2024-10-10 17:46:15 -04:00
|
|
|
}}> This won't take long! </p>
|
|
|
|
|
|
|
|
|
|
</div>
|
2024-06-01 14:32:22 -04:00
|
|
|
) : (
|
|
|
|
|
<>
|
2024-06-24 14:53:58 -04:00
|
|
|
{hideWarningCard ? null : (
|
2024-07-07 00:38:05 -04:00
|
|
|
<ErrorCard>
|
2024-06-24 14:53:58 -04:00
|
|
|
<Forms.FormTitle tag="h4">Want your theme removed?</Forms.FormTitle>
|
|
|
|
|
<Forms.FormText className={Margins.top8}>
|
|
|
|
|
If you want your theme(s) permanently removed, please open an issue on <a href="https://github.com/Faf4a/plugins/issues/new?labels=removal&projects=&template=request_removal.yml&title=Theme+Removal">GitHub <OpenExternalIcon height={16} width={16} /></a>
|
|
|
|
|
</Forms.FormText>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
Settings.plugins.ThemeLibrary.hideWarningCard = true;
|
|
|
|
|
setHideWarningCard(true);
|
|
|
|
|
}}
|
|
|
|
|
size={Button.Sizes.SMALL}
|
|
|
|
|
color={Button.Colors.RED}
|
|
|
|
|
look={Button.Looks.FILLED}
|
2024-07-07 00:38:05 -04:00
|
|
|
className={classes(Margins.top16, "vce-warning-button")}
|
2024-06-24 14:53:58 -04:00
|
|
|
>Hide</Button>
|
2024-10-10 17:46:15 -04:00
|
|
|
</ErrorCard>
|
2024-06-24 14:53:58 -04:00
|
|
|
)}
|
2024-07-12 11:31:17 -04:00
|
|
|
<div className={classes(Margins.bottom8, Margins.top16)}>
|
2024-06-01 14:32:22 -04:00
|
|
|
<Forms.FormTitle tag="h2"
|
|
|
|
|
style={{
|
2024-05-04 00:50:59 -04:00
|
|
|
overflowWrap: "break-word",
|
|
|
|
|
marginTop: 8,
|
2024-06-01 14:32:22 -04:00
|
|
|
}}>
|
2024-07-12 11:31:17 -04:00
|
|
|
{searchValue.status === SearchStatus.LIKED ? "Most Liked" : "Newest Additions"}
|
2024-06-01 14:32:22 -04:00
|
|
|
</Forms.FormTitle>
|
|
|
|
|
|
|
|
|
|
{themes.slice(0, 2).map((theme: Theme) => (
|
2024-10-10 17:46:15 -04:00
|
|
|
<ThemeCard
|
|
|
|
|
key={theme.id}
|
|
|
|
|
theme={theme}
|
|
|
|
|
themeLinks={themeLinks}
|
|
|
|
|
likedThemes={likedThemes}
|
|
|
|
|
setThemeLinks={setThemeLinks}
|
|
|
|
|
removePreview={true}
|
|
|
|
|
/>
|
2024-06-01 14:32:22 -04:00
|
|
|
))}
|
2024-05-04 00:50:59 -04:00
|
|
|
</div>
|
2024-06-01 14:32:22 -04:00
|
|
|
<Forms.FormTitle tag="h2" style={{
|
|
|
|
|
overflowWrap: "break-word",
|
|
|
|
|
marginTop: 20,
|
|
|
|
|
}}>
|
|
|
|
|
Themes
|
|
|
|
|
</Forms.FormTitle>
|
2024-07-12 11:31:17 -04:00
|
|
|
<div className={classes(Margins.bottom20, "vce-search-grid")}>
|
2024-06-01 14:32:22 -04:00
|
|
|
<TextInput value={searchValue.value} placeholder="Search for a theme..." onChange={onSearch} />
|
|
|
|
|
<div className={InputStyles.inputWrapper}>
|
2024-07-07 00:38:05 -04:00
|
|
|
<SearchableSelect
|
2024-06-01 14:32:22 -04:00
|
|
|
options={[
|
|
|
|
|
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
|
|
|
|
{ label: "Show Themes", value: SearchStatus.THEME },
|
|
|
|
|
{ label: "Show Snippets", value: SearchStatus.SNIPPET },
|
2024-07-12 11:31:17 -04:00
|
|
|
{ label: "Show Most Liked", value: SearchStatus.LIKED },
|
2024-06-01 14:32:22 -04:00
|
|
|
{ label: "Show Dark", value: SearchStatus.DARK },
|
|
|
|
|
{ label: "Show Light", value: SearchStatus.LIGHT },
|
2024-06-24 14:53:58 -04:00
|
|
|
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
|
|
|
|
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
2024-06-01 14:32:22 -04:00
|
|
|
]}
|
2024-07-07 00:38:05 -04:00
|
|
|
// @ts-ignore
|
|
|
|
|
value={searchValue.status}
|
|
|
|
|
clearable={false}
|
|
|
|
|
onChange={v => onStatusChange(v as SearchStatus)}
|
2024-06-01 14:32:22 -04:00
|
|
|
closeOnSelect={true}
|
2024-07-07 00:38:05 -04:00
|
|
|
className={InputStyles.inputDefault}
|
2024-05-04 00:50:59 -04:00
|
|
|
/>
|
2024-06-01 14:32:22 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
2024-10-10 17:46:15 -04:00
|
|
|
{filteredThemes.length ? filteredThemes.map((theme: Theme) => (
|
|
|
|
|
<ThemeCard
|
|
|
|
|
key={theme.id}
|
|
|
|
|
theme={theme}
|
|
|
|
|
themeLinks={themeLinks}
|
|
|
|
|
likedThemes={likedThemes}
|
|
|
|
|
setThemeLinks={setThemeLinks}
|
|
|
|
|
/>
|
|
|
|
|
)) : <div
|
|
|
|
|
style={{
|
2024-06-01 14:32:22 -04:00
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
2024-10-10 17:46:15 -04:00
|
|
|
justifyContent: "center",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
}}>
|
|
|
|
|
<p style={{
|
|
|
|
|
fontSize: "1em",
|
|
|
|
|
color: "var(--text-normal)"
|
|
|
|
|
}}> No theme found. </p>
|
|
|
|
|
<p style={{
|
|
|
|
|
fontSize: ".75em",
|
|
|
|
|
color: "var(--text-muted)"
|
|
|
|
|
}}> Try narrowing your search down. </p>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2024-06-01 14:32:22 -04:00
|
|
|
</div>
|
|
|
|
|
</>)}
|
2024-05-04 00:50:59 -04:00
|
|
|
</>
|
2024-10-10 17:46:15 -04:00
|
|
|
</div>
|
2024-05-04 00:50:59 -04:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-10 17:46:15 -04:00
|
|
|
// rework this!
|
2024-05-04 00:50:59 -04:00
|
|
|
function SubmitThemes() {
|
2024-10-10 17:46:15 -04:00
|
|
|
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]);
|
2024-05-04 00:50:59 -04:00
|
|
|
|
|
|
|
|
return (
|
2024-07-12 11:31:17 -04:00
|
|
|
<div className={classes(Margins.bottom8, Margins.top16)}>
|
2024-05-04 00:50:59 -04:00
|
|
|
<Forms.FormTitle tag="h2" style={{
|
|
|
|
|
overflowWrap: "break-word",
|
|
|
|
|
marginTop: 8,
|
2024-10-10 17:46:15 -04:00
|
|
|
marginBottom: 8,
|
2024-05-04 00:50:59 -04:00
|
|
|
}}>
|
2024-10-10 17:46:15 -04:00
|
|
|
Submission Guidelines
|
2024-05-04 00:50:59 -04:00
|
|
|
</Forms.FormTitle>
|
2024-10-10 17:46:15 -04:00
|
|
|
<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>
|
|
|
|
|
|
2024-05-04 00:50:59 -04:00
|
|
|
<Forms.FormTitle tag="h2" style={{
|
|
|
|
|
overflowWrap: "break-word",
|
|
|
|
|
marginTop: 8,
|
2024-10-10 17:46:15 -04:00
|
|
|
marginBottom: 8,
|
2024-05-04 00:50:59 -04:00
|
|
|
}}>
|
2024-10-10 17:46:15 -04:00
|
|
|
{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>
|
|
|
|
|
|
|
|
|
|
|
2024-05-04 00:50:59 -04:00
|
|
|
<Forms.FormText>
|
2024-10-10 17:46:15 -04:00
|
|
|
{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>
|
2024-07-07 00:38:05 -04:00
|
|
|
|
2024-10-10 17:46:15 -04:00
|
|
|
<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`,
|
2024-07-07 00:38:05 -04:00
|
|
|
id: Toasts.genId(),
|
|
|
|
|
type: Toasts.Type.FAILURE,
|
|
|
|
|
options: {
|
2024-10-10 17:46:15 -04:00
|
|
|
duration: 2e3,
|
|
|
|
|
position: Toasts.Position.BOTTOM
|
2024-07-07 00:38:05 -04:00
|
|
|
}
|
|
|
|
|
});
|
2024-10-10 17:46:15 -04:00
|
|
|
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();
|
2024-07-07 00:38:05 -04:00
|
|
|
|
|
|
|
|
const token = await DataStore.get("ThemeLibrary_uniqueToken");
|
2024-05-04 00:50:59 -04:00
|
|
|
|
2024-10-10 17:46:15 -04:00
|
|
|
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", {
|
2024-05-04 00:50:59 -04:00
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
2024-07-07 00:38:05 -04:00
|
|
|
token,
|
2024-10-10 17:46:15 -04:00
|
|
|
title: theme.title,
|
|
|
|
|
type: theme.type,
|
|
|
|
|
description: theme.description,
|
|
|
|
|
version: theme.description,
|
|
|
|
|
content: theme.content,
|
|
|
|
|
attribution: theme.attribution,
|
|
|
|
|
screenshotMetadata: theme.screenshotMetadata
|
|
|
|
|
})
|
2024-07-07 00:38:05 -04:00
|
|
|
}).then(async response => {
|
2024-05-04 00:50:59 -04:00
|
|
|
if (!response.ok) {
|
2024-07-07 00:38:05 -04:00
|
|
|
const res = await response.json();
|
2024-10-10 17:46:15 -04:00
|
|
|
logger.debug(theme);
|
|
|
|
|
return Toasts.show({
|
|
|
|
|
message: res.message,
|
2024-05-04 00:50:59 -04:00
|
|
|
id: Toasts.genId(),
|
|
|
|
|
type: Toasts.Type.FAILURE,
|
|
|
|
|
options: {
|
|
|
|
|
duration: 5e3,
|
|
|
|
|
position: Toasts.Position.BOTTOM
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
Toasts.show({
|
2024-10-10 17:46:15 -04:00
|
|
|
message: "Theme submitted successfully!",
|
2024-05-04 00:50:59 -04:00
|
|
|
id: Toasts.genId(),
|
2024-10-10 17:46:15 -04:00
|
|
|
type: Toasts.Type.SUCCESS,
|
2024-05-04 00:50:59 -04:00
|
|
|
options: {
|
2024-10-10 17:46:15 -04:00
|
|
|
duration: 3.5e3,
|
2024-05-04 00:50:59 -04:00
|
|
|
position: Toasts.Position.BOTTOM
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-10-10 17:46:15 -04:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-05-04 00:50:59 -04:00
|
|
|
}
|
2024-10-10 17:46:15 -04:00
|
|
|
}).catch(err => {
|
|
|
|
|
return Toasts.show({
|
|
|
|
|
message: err.message,
|
2024-07-07 00:38:05 -04:00
|
|
|
id: Toasts.genId(),
|
2024-10-10 17:46:15 -04:00
|
|
|
type: Toasts.Type.FAILURE,
|
2024-07-07 00:38:05 -04:00
|
|
|
options: {
|
|
|
|
|
duration: 5e3,
|
|
|
|
|
position: Toasts.Position.BOTTOM
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-05-04 00:50:59 -04:00
|
|
|
});
|
2024-10-10 17:46:15 -04:00
|
|
|
logger.debug(theme);
|
2024-05-04 00:50:59 -04:00
|
|
|
}}
|
|
|
|
|
size={Button.Sizes.MEDIUM}
|
|
|
|
|
color={Button.Colors.GREEN}
|
|
|
|
|
look={Button.Looks.FILLED}
|
|
|
|
|
className={Margins.top16}
|
2024-10-10 17:46:15 -04:00
|
|
|
disabled={!valid}
|
2024-05-04 00:50:59 -04:00
|
|
|
>
|
|
|
|
|
Submit
|
|
|
|
|
</Button>
|
2024-10-10 17:46:15 -04:00
|
|
|
<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>
|
2024-05-04 00:50:59 -04:00
|
|
|
<p style={{
|
|
|
|
|
color: "var(--text-muted)",
|
|
|
|
|
fontSize: "12px",
|
|
|
|
|
marginTop: "8px",
|
2024-10-10 17:46:15 -04:00
|
|
|
marginLeft: "12px",
|
2024-05-04 00:50:59 -04:00
|
|
|
}}>
|
2024-07-07 00:38:05 -04:00
|
|
|
Abusing this feature will result in you being blocked from further submissions.
|
2024-05-04 00:50:59 -04:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Forms.FormText>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-10 17:46:15 -04:00
|
|
|
|
2024-05-04 00:50:59 -04:00
|
|
|
function ThemeLibrary() {
|
|
|
|
|
const [currentTab, setCurrentTab] = useState(TabItem.THEMES);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SettingsTab title="Theme Library">
|
|
|
|
|
<TabBar
|
|
|
|
|
type="top"
|
|
|
|
|
look="brand"
|
|
|
|
|
className="vc-settings-tab-bar"
|
|
|
|
|
selectedItem={currentTab}
|
|
|
|
|
onItemSelect={setCurrentTab}
|
|
|
|
|
>
|
|
|
|
|
<TabBar.Item
|
|
|
|
|
className="vc-settings-tab-bar-item"
|
|
|
|
|
id={TabItem.THEMES}
|
|
|
|
|
>
|
|
|
|
|
Themes
|
|
|
|
|
</TabBar.Item>
|
|
|
|
|
<TabBar.Item
|
|
|
|
|
className="vc-settings-tab-bar-item"
|
|
|
|
|
id={TabItem.SUBMIT_THEMES}
|
|
|
|
|
>
|
|
|
|
|
Submit Theme
|
|
|
|
|
</TabBar.Item>
|
|
|
|
|
</TabBar>
|
|
|
|
|
|
|
|
|
|
{currentTab === TabItem.THEMES ? <ThemeTab /> : <SubmitThemes />}
|
|
|
|
|
</SettingsTab>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 21:46:46 -04:00
|
|
|
export default wrapTab(ThemeLibrary, "Theme Library");
|