mirror of
https://github.com/Equicord/Equicord.git
synced 2025-03-04 08:20:02 -05:00
Requests & Updates
This commit is contained in:
parent
f2328dea7f
commit
1a37064f2e
10 changed files with 952 additions and 32 deletions
149
src/equicordplugins/friendshipRanks/index.tsx
Normal file
149
src/equicordplugins/friendshipRanks/index.tsx
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { Modals, ModalSize, openModal } from "@utils/modal";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Flex, Forms, RelationshipStore } from "@webpack/common";
|
||||||
|
|
||||||
|
interface rankInfo {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
requirement: number;
|
||||||
|
assetURL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysSince(dateString: string): number {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const currentDate = new Date();
|
||||||
|
|
||||||
|
const differenceInMs = currentDate.getTime() - date.getTime();
|
||||||
|
|
||||||
|
const days = differenceInMs / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
return Math.floor(days);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranks: rankInfo[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Sprout",
|
||||||
|
description: "Your friendship is just starting",
|
||||||
|
requirement: 0,
|
||||||
|
assetURL: "https://files.catbox.moe/d6gis2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Blooming",
|
||||||
|
description: "Your friendship is getting there! (1 Month)",
|
||||||
|
requirement: 30,
|
||||||
|
assetURL: "https://files.catbox.moe/z7fxjq.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Burning",
|
||||||
|
description: "Your friendship has reached terminal velocity :o (3 Months)",
|
||||||
|
requirement: 90,
|
||||||
|
assetURL: "https://files.catbox.moe/8oiu0o.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Star",
|
||||||
|
description: "Your friendship has been going on for a WHILE (1 Year)",
|
||||||
|
requirement: 365,
|
||||||
|
assetURL: "https://files.catbox.moe/7bpe7v.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Royal",
|
||||||
|
description: "Your friendship has gone through thick and thin- a whole 2 years!",
|
||||||
|
requirement: 730,
|
||||||
|
assetURL: "https://files.catbox.moe/0yp9mp.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Besties",
|
||||||
|
description: "How do you even manage this??? (5 Years)",
|
||||||
|
assetURL: "https://files.catbox.moe/qojb7d.webp",
|
||||||
|
requirement: 1826.25
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function openRankModal(rank: rankInfo) {
|
||||||
|
openModal(props => (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Modals.ModalRoot {...props} size={ModalSize.DYNAMIC}>
|
||||||
|
<Modals.ModalHeader>
|
||||||
|
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||||
|
<Forms.FormTitle
|
||||||
|
tag="h2"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
margin: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Your friendship rank
|
||||||
|
</Forms.FormTitle>
|
||||||
|
</Flex>
|
||||||
|
</Modals.ModalHeader>
|
||||||
|
<Modals.ModalContent>
|
||||||
|
<div style={{ padding: "1em", textAlign: "center" }}>
|
||||||
|
<Forms.FormText className={Margins.bottom20}>
|
||||||
|
{rank.title}
|
||||||
|
</Forms.FormText>
|
||||||
|
<img src={rank.assetURL} style={{ height: "150px" }} />
|
||||||
|
<Forms.FormText className={Margins.top16}>
|
||||||
|
{rank.description}
|
||||||
|
</Forms.FormText>
|
||||||
|
</div>
|
||||||
|
</Modals.ModalContent>
|
||||||
|
</Modals.ModalRoot>
|
||||||
|
</ErrorBoundary >
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBadgesToApply() {
|
||||||
|
|
||||||
|
const badgesToApply: ProfileBadge[] = ranks.map((rank, index, self) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
description: rank.title,
|
||||||
|
image: rank.assetURL,
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
transform: "scale(0.8)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldShow: (info: BadgeUserArgs) => {
|
||||||
|
if (!RelationshipStore.isFriend(info.user.id)) { return false; }
|
||||||
|
|
||||||
|
const days = daysSince(RelationshipStore.getSince(info.user.id));
|
||||||
|
|
||||||
|
if (self[index + 1] == null) {
|
||||||
|
return days > rank.requirement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (days > rank.requirement && days < self[index + 1].requirement);
|
||||||
|
},
|
||||||
|
onClick: () => openRankModal(rank)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return badgesToApply;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "FriendshipRanks",
|
||||||
|
description: "Adds badges showcasing how long you have been friends with a user for",
|
||||||
|
authors: [
|
||||||
|
Devs.Samwich
|
||||||
|
],
|
||||||
|
start() {
|
||||||
|
getBadgesToApply().forEach(thing => Vencord.Api.Badges.addBadge(thing));
|
||||||
|
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
getBadgesToApply().forEach(thing => Vencord.Api.Badges.removeBadge(thing));
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "ImageLink",
|
|
||||||
description: "Suppresses the hiding of links for \"simple embeds\"",
|
|
||||||
authors: [Devs.Kyuuhachi],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "isEmbedInline:function",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=isEmbedInline:function\(\)\{return )\w+(?=\})/,
|
|
||||||
replace: "()=>false",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
154
src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx
Normal file
154
src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
|
import { Heart } from "@components/Heart";
|
||||||
|
import { openInviteModal } from "@utils/discord";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||||
|
import { findComponentByCodeLazy } from "@webpack";
|
||||||
|
import { Button, Clipboard, Forms, React, showToast, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
|
import { ThemeInfoModalProps } from "../types";
|
||||||
|
|
||||||
|
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||||
|
|
||||||
|
export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, ...props }) => {
|
||||||
|
|
||||||
|
const content = atob(theme.content);
|
||||||
|
const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
|
||||||
|
const donate = metadata.match(/@donate\s+(.+)/)?.[1] || "";
|
||||||
|
const version = metadata.match(/@version\s+(.+)/)?.[1] || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalRoot {...props}>
|
||||||
|
<ModalHeader>
|
||||||
|
<Forms.FormTitle tag="h4">Theme Details</Forms.FormTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalContent>
|
||||||
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Author</Forms.FormTitle>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", marginBottom: "10px" }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<UserSummaryItem
|
||||||
|
users={[author]}
|
||||||
|
guildId={undefined}
|
||||||
|
renderIcon={false}
|
||||||
|
showDefaultAvatarsForNullUsers
|
||||||
|
size={32}
|
||||||
|
showUserPopout
|
||||||
|
className={Margins.right8}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormText>
|
||||||
|
{author.username}
|
||||||
|
</Forms.FormText>
|
||||||
|
</div>
|
||||||
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Source</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
<Button onClick={() => openModal(modalProps => (
|
||||||
|
<ModalRoot {...modalProps}>
|
||||||
|
<ModalHeader>
|
||||||
|
<Forms.FormTitle tag="h4">Theme Source</Forms.FormTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalContent>
|
||||||
|
<Forms.FormText style={{
|
||||||
|
padding: "5px",
|
||||||
|
}}>
|
||||||
|
<CodeBlock lang="css" content={content} />
|
||||||
|
</Forms.FormText>
|
||||||
|
</ModalContent>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
look={Button.Looks.OUTLINED}
|
||||||
|
onClick={() => modalProps.onClose()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button className={Margins.right8}
|
||||||
|
onClick={() => {
|
||||||
|
Clipboard.copy(content);
|
||||||
|
showToast("Copied to Clipboard", Toasts.Type.SUCCESS);
|
||||||
|
}}>Copy to Clipboard</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalRoot>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
View Theme Source
|
||||||
|
</Button>
|
||||||
|
</Forms.FormText>
|
||||||
|
{version && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Version</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
{version}
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{donate && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Donate</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
You can support the author by donating below.
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormText style={{ marginTop: "10px" }}>
|
||||||
|
<Button onClick={() => VencordNative.native.openExternal(donate)}>
|
||||||
|
<Heart />
|
||||||
|
Donate
|
||||||
|
</Button>
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{theme.guild && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Support Server</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
{theme.guild.name}
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormText>
|
||||||
|
<Button
|
||||||
|
color={Button.Colors.BRAND_NEW}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
className={Margins.top8}
|
||||||
|
onClick={async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
theme.guild?.invite_link != null && openInviteModal(theme.guild?.invite_link.split("discord.gg/")[1]).catch(() => showToast("Invalid or expired invite!", Toasts.Type.FAILURE));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Join Discord Server
|
||||||
|
</Button>
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{theme.tags && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Tags</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
{theme.tags.map(tag => (
|
||||||
|
<span className="vce-theme-info-tag">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
look={Button.Looks.OUTLINED}
|
||||||
|
onClick={() => props.onClose()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalRoot>
|
||||||
|
);
|
||||||
|
};
|
496
src/equicordplugins/themeLibrary/components/ThemeTab.tsx
Normal file
496
src/equicordplugins/themeLibrary/components/ThemeTab.tsx
Normal file
|
@ -0,0 +1,496 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { generateId } from "@api/Commands";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
|
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
|
||||||
|
import { proxyLazy } from "@utils/lazy";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
|
import { openModal } from "@utils/modal";
|
||||||
|
import { findByPropsLazy, findLazy } from "@webpack";
|
||||||
|
import { Button, Card, FluxDispatcher, Forms, React, Select, showToast, TabBar, TextArea, TextInput, Toasts, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
|
import { SearchStatus, TabItem, Theme } from "../types";
|
||||||
|
import { ThemeInfoModal } from "./ThemeInfoModal";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-plugins-");
|
||||||
|
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||||
|
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||||
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
|
const API_URL = "https://themes-delta.vercel.app/api";
|
||||||
|
|
||||||
|
async function themeRequest(path: string, options: RequestInit = {}) {
|
||||||
|
return fetch(API_URL + path, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {
|
||||||
|
const newUser = new UserRecord({
|
||||||
|
username: user.username,
|
||||||
|
id: user.id ?? generateId(),
|
||||||
|
avatar: user.avatar,
|
||||||
|
bot: true,
|
||||||
|
});
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "USER_UPDATE",
|
||||||
|
user: newUser,
|
||||||
|
});
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeTemplate = `/**
|
||||||
|
* @name [Theme name]
|
||||||
|
* @author [Your name]
|
||||||
|
* @description [Your Theme Description]
|
||||||
|
* @version [Your Theme Version]
|
||||||
|
* @donate [Optionally, your Donation Link]
|
||||||
|
* @tags [Optionally, tags that apply to your theme]
|
||||||
|
* @invite [Optionally, your Support Server Invite]
|
||||||
|
* @source [Optionally, your source code link]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Your CSS goes here */
|
||||||
|
`;
|
||||||
|
|
||||||
|
function ThemeTab() {
|
||||||
|
const [themes, setThemes] = useState<Theme[]>([]);
|
||||||
|
const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]);
|
||||||
|
const [themeLinks, setThemeLinks] = useState(Vencord.Settings.themeLinks);
|
||||||
|
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
|
||||||
|
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||||
|
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||||
|
|
||||||
|
const themeFilter = (theme: Theme) => {
|
||||||
|
const enabled = themeLinks.includes("https://themes-delta.vercel.app/api/" + theme.name);
|
||||||
|
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
||||||
|
if (!theme.tags.includes("theme") && searchValue.status === SearchStatus.THEME) return false;
|
||||||
|
if (!theme.tags.includes("snippet") && searchValue.status === SearchStatus.SNIPPET) return false;
|
||||||
|
if (!theme.tags.includes("dark") && searchValue.status === SearchStatus.DARK) return false;
|
||||||
|
if (!theme.tags.includes("light") && searchValue.status === SearchStatus.LIGHT) return false;
|
||||||
|
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
||||||
|
if (!searchValue.value.length) return true;
|
||||||
|
|
||||||
|
const v = searchValue.value.toLowerCase();
|
||||||
|
return (
|
||||||
|
theme.name.toLowerCase().includes(v) ||
|
||||||
|
theme.description.toLowerCase().includes(v) ||
|
||||||
|
theme.author.discord_name.toLowerCase().includes(v) ||
|
||||||
|
theme.tags?.some(t => t.toLowerCase().includes(v))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
themeRequest("/themes", {
|
||||||
|
method: "GET",
|
||||||
|
}).then(async (response: Response) => {
|
||||||
|
const data = await response.json();
|
||||||
|
const themes: Theme[] = Object.values(data);
|
||||||
|
themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime());
|
||||||
|
setThemes(themes);
|
||||||
|
setFilteredThemes(themes);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setThemeLinks(Vencord.Settings.themeLinks);
|
||||||
|
}, [Vencord.Settings.themeLinks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filteredThemes = themes.filter(themeFilter);
|
||||||
|
setFilteredThemes(filteredThemes);
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
{loading ? (
|
||||||
|
<div
|
||||||
|
className={Margins.top20}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
fontSize: "1.5em",
|
||||||
|
color: "var(--text-muted)"
|
||||||
|
}}>Loading Themes...</div>
|
||||||
|
) : (<>
|
||||||
|
<div className={`${Margins.bottom8} ${Margins.top16}`}>
|
||||||
|
<Forms.FormTitle tag="h2"
|
||||||
|
style={{
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
marginTop: 8,
|
||||||
|
}}>
|
||||||
|
Newest Additions
|
||||||
|
</Forms.FormTitle>
|
||||||
|
|
||||||
|
{themes.slice(0, 2).map((theme: Theme) => (
|
||||||
|
<Card style={{
|
||||||
|
padding: ".5rem",
|
||||||
|
marginBottom: ".5em",
|
||||||
|
marginTop: ".5em",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
backgroundColor: "var(--background-secondary-alt)"
|
||||||
|
}} key={theme.id}>
|
||||||
|
<Forms.FormTitle tag="h2" style={{
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
marginTop: 8,
|
||||||
|
}}
|
||||||
|
className="vce-theme-text">
|
||||||
|
{theme.name}
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<Forms.FormText className="vce-theme-text">
|
||||||
|
{theme.description}
|
||||||
|
</Forms.FormText>
|
||||||
|
<div className="vce-theme-info">
|
||||||
|
<div style={{
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}>
|
||||||
|
{theme.tags && (
|
||||||
|
<Forms.FormText>
|
||||||
|
{theme.tags.map(tag => (
|
||||||
|
<span className="vce-theme-info-tag">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</Forms.FormText>
|
||||||
|
)}
|
||||||
|
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
||||||
|
{themeLinks.includes("https://themes-delta.vercel.app/api/" + theme.name) ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const onlineThemeLinks = themeLinks.filter(x => x !== "https://themes-delta.vercel.app/api/" + theme.name);
|
||||||
|
setThemeLinks(onlineThemeLinks);
|
||||||
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
className={Margins.right8}
|
||||||
|
>
|
||||||
|
Remove Theme
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const onlineThemeLinks = [...themeLinks, `https://themes-delta.vercel.app/api/${theme.name}`];
|
||||||
|
setThemeLinks(onlineThemeLinks);
|
||||||
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.GREEN}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
className={Margins.right8}
|
||||||
|
>
|
||||||
|
Add Theme
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const author = await getUser(theme.author.discord_snowflake, theme.author.discord_name);
|
||||||
|
openModal(props => <ThemeInfoModal {...props} author={author} theme={theme} />);
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.BRAND}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
>
|
||||||
|
Theme Info
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => VencordNative.native.openExternal(`https://themes-delta.vercel.app/api/${theme.name}`)}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.LINK}
|
||||||
|
look={Button.Looks.LINK}
|
||||||
|
>
|
||||||
|
View Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Forms.FormTitle tag="h2" style={{
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
marginTop: 20,
|
||||||
|
}}>
|
||||||
|
Themes
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<div className={cl("filter-controls")}>
|
||||||
|
<TextInput value={searchValue.value} placeholder="Search for a theme..." onChange={onSearch} />
|
||||||
|
<div className={InputStyles.inputWrapper}>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||||
|
{ label: "Show Themes", value: SearchStatus.THEME },
|
||||||
|
{ label: "Show Snippets", value: SearchStatus.SNIPPET },
|
||||||
|
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||||
|
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||||
|
{ label: "Show Dark", value: SearchStatus.DARK },
|
||||||
|
{ label: "Show Light", value: SearchStatus.LIGHT },
|
||||||
|
]}
|
||||||
|
serialize={String}
|
||||||
|
select={onStatusChange}
|
||||||
|
isSelected={v => v === searchValue.status}
|
||||||
|
closeOnSelect={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{filteredThemes.map((theme: Theme) => (
|
||||||
|
<Card style={{
|
||||||
|
padding: ".5rem",
|
||||||
|
marginBottom: ".5em",
|
||||||
|
marginTop: ".5em",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
backgroundColor: "var(--background-secondary-alt)"
|
||||||
|
}} key={theme.id}>
|
||||||
|
<Forms.FormTitle tag="h2" style={{
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
marginTop: 8,
|
||||||
|
}}
|
||||||
|
className="vce-theme-text">
|
||||||
|
{theme.name}
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<Forms.FormText className="vce-theme-text">
|
||||||
|
{theme.description}
|
||||||
|
</Forms.FormText>
|
||||||
|
<img
|
||||||
|
role="presentation"
|
||||||
|
src={theme.thumbnail_url}
|
||||||
|
alt="Theme Preview Image"
|
||||||
|
className="vce-theme-info-preview"
|
||||||
|
/>
|
||||||
|
<div className="vce-theme-info">
|
||||||
|
<div style={{
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}>
|
||||||
|
{theme.tags && (
|
||||||
|
<Forms.FormText>
|
||||||
|
{theme.tags.map(tag => (
|
||||||
|
<span className="vce-theme-info-tag">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</Forms.FormText>
|
||||||
|
)}
|
||||||
|
<div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
|
||||||
|
{themeLinks.includes("https://themes-delta.vercel.app/api/" + theme.name) ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const onlineThemeLinks = themeLinks.filter(x => x !== "https://themes-delta.vercel.app/api/" + theme.name);
|
||||||
|
setThemeLinks(onlineThemeLinks);
|
||||||
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
className={Margins.right8}
|
||||||
|
>
|
||||||
|
Remove Theme
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const onlineThemeLinks = [...themeLinks, `https://themes-delta.vercel.app/api/${theme.name}`];
|
||||||
|
setThemeLinks(onlineThemeLinks);
|
||||||
|
Vencord.Settings.themeLinks = onlineThemeLinks;
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.GREEN}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
className={Margins.right8}
|
||||||
|
>
|
||||||
|
Add Theme
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const author = await getUser(theme.author.discord_snowflake, theme.author.discord_name);
|
||||||
|
openModal(props => <ThemeInfoModal {...props} author={author} theme={theme} />);
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.BRAND}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
>
|
||||||
|
Theme Info
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => VencordNative.native.openExternal(`https://themes-delta.vercel.app/api/${theme.name}`)}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.LINK}
|
||||||
|
look={Button.Looks.LINK}
|
||||||
|
>
|
||||||
|
View Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
</>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubmitThemes() {
|
||||||
|
const currentUser = UserStore.getCurrentUser();
|
||||||
|
const [themeContent, setContent] = useState("");
|
||||||
|
|
||||||
|
const handleChange = (v: string) => setContent(v);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${Margins.bottom8} ${Margins.top16}`}>
|
||||||
|
<Forms.FormTitle tag="h2" style={{
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
marginTop: 8,
|
||||||
|
}}>
|
||||||
|
Theme Guidelines
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
Follow the formatting for your CSS to get credit for your theme. You can find the template below.
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormText>
|
||||||
|
(your theme will be reviewed and can take up to 24 hours to be approved)
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormText className={`${Margins.bottom16} ${Margins.top8}`}>
|
||||||
|
<CodeBlock lang="css" content={themeTemplate} />
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormTitle tag="h2" style={{
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
marginTop: 8,
|
||||||
|
}}>
|
||||||
|
Submit Themes
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
<TextArea
|
||||||
|
content={themeTemplate}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={classes(TextAreaProps.textarea, "vce-text-input")}
|
||||||
|
placeholder="Theme CSS goes here..."
|
||||||
|
spellCheck={false}
|
||||||
|
rows={35}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (themeContent.length < 50) return showToast("Theme content is too short, must be above 50", Toasts.Type.FAILURE);
|
||||||
|
|
||||||
|
themeRequest("/submit/theme", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: `${currentUser.id}`,
|
||||||
|
content: btoa(themeContent),
|
||||||
|
}),
|
||||||
|
}).then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
Toasts.show({
|
||||||
|
message: "Failed to submit theme, try again later. Probably ratelimit, wait 2 minutes.",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
options: {
|
||||||
|
duration: 5e3,
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Toasts.show({
|
||||||
|
message: "Submitted your theme! Review can take up to 24 hours.",
|
||||||
|
type: Toasts.Type.SUCCESS,
|
||||||
|
id: Toasts.genId(),
|
||||||
|
options: {
|
||||||
|
duration: 5e3,
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
showToast("Failed to submit theme, try later", Toasts.Type.FAILURE);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.GREEN}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
className={Margins.top16}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<p style={{
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "8px",
|
||||||
|
marginLeft: "8px",
|
||||||
|
}}>
|
||||||
|
By submitting your theme, you agree to your Discord User ID being processed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Forms.FormText>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default wrapTab(ThemeLibrary, "Theme Library");
|
54
src/equicordplugins/themeLibrary/components/styles.css
Normal file
54
src/equicordplugins/themeLibrary/components/styles.css
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/* stylelint-disable property-no-vendor-prefix */
|
||||||
|
[data-tab-id="ThemeLibrary"]::before {
|
||||||
|
-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vce-theme-info-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vce-theme-info-preview img {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vce-theme-text {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vce-text-input {
|
||||||
|
display: inline-block !important;
|
||||||
|
color: var(--text-normal) !important;
|
||||||
|
font-family: var(--font-code) !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 2px solid var(--background-tertiary);
|
||||||
|
max-height: unset;
|
||||||
|
}
|
39
src/equicordplugins/themeLibrary/index.ts
Normal file
39
src/equicordplugins/themeLibrary/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EquicordDevs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "Theme Library",
|
||||||
|
description: "A library of themes for Vencord.",
|
||||||
|
authors: [EquicordDevs.Fafa],
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const customSettingsSections = (
|
||||||
|
Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record<string, unknown>) => any)[]; }
|
||||||
|
).customSections;
|
||||||
|
|
||||||
|
const ThemeSection = () => ({
|
||||||
|
section: "ThemeLibrary",
|
||||||
|
label: "Theme Library",
|
||||||
|
element: require("./components/ThemeTab").default,
|
||||||
|
id: "ThemeSection"
|
||||||
|
});
|
||||||
|
|
||||||
|
customSettingsSections.push(ThemeSection);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
const customSettingsSections = (
|
||||||
|
Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record<string, unknown>) => any)[]; }
|
||||||
|
).customSections;
|
||||||
|
|
||||||
|
const i = customSettingsSections.findIndex(section => section({}).id === "ThemeSection");
|
||||||
|
|
||||||
|
if (i !== -1) customSettingsSections.splice(i, 1);
|
||||||
|
}
|
||||||
|
});
|
56
src/equicordplugins/themeLibrary/types.ts
Normal file
56
src/equicordplugins/themeLibrary/types.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ModalProps } from "@utils/modal";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
export interface Theme {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
file_name: string;
|
||||||
|
content: string;
|
||||||
|
type: string | "theme" | "snippet";
|
||||||
|
description: string;
|
||||||
|
external_url?: string;
|
||||||
|
download_url: string;
|
||||||
|
version?: string;
|
||||||
|
author: {
|
||||||
|
github_name?: string;
|
||||||
|
discord_name: string;
|
||||||
|
discord_snowflake: string;
|
||||||
|
};
|
||||||
|
likes?: number;
|
||||||
|
downloads?: number;
|
||||||
|
tags: string[];
|
||||||
|
thumbnail_url: string;
|
||||||
|
release_date: string;
|
||||||
|
guild?: {
|
||||||
|
name: string;
|
||||||
|
snowflake: string;
|
||||||
|
invite_link: string;
|
||||||
|
avatar_hash: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeInfoModalProps extends ModalProps {
|
||||||
|
author: User;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TabItem {
|
||||||
|
THEMES,
|
||||||
|
SUBMIT_THEMES,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum SearchStatus {
|
||||||
|
ALL,
|
||||||
|
ENABLED,
|
||||||
|
DISABLED,
|
||||||
|
THEME,
|
||||||
|
SNIPPET,
|
||||||
|
DARK,
|
||||||
|
LIGHT,
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
# ImageLink
|
|
||||||
|
|
||||||
If a message consists of only a link to an image, Discord hides the link and shows only the image embed. This plugin makes the link show regardless.
|
|
|
@ -1,5 +0,0 @@
|
||||||
# PauseInvitesForever
|
|
||||||
|
|
||||||
Adds a button to the Security Actions modal to to pause invites indefinitely.
|
|
||||||
|
|
||||||

|
|
|
@ -599,6 +599,10 @@ export const EquicordDevs = Object.freeze({
|
||||||
name: "kvba",
|
name: "kvba",
|
||||||
id: 105170831130234880n,
|
id: 105170831130234880n,
|
||||||
},
|
},
|
||||||
|
Fafa: {
|
||||||
|
name: "Fafa",
|
||||||
|
id: 428188716641812481n,
|
||||||
|
},
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
// iife so #__PURE__ works correctly
|
// iife so #__PURE__ works correctly
|
||||||
|
|
Loading…
Add table
Reference in a new issue