diff --git a/src/equicordplugins/friendshipRanks/index.tsx b/src/equicordplugins/friendshipRanks/index.tsx new file mode 100644 index 00000000..52d0f1fa --- /dev/null +++ b/src/equicordplugins/friendshipRanks/index.tsx @@ -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 => ( + + + + + + Your friendship rank + + + + + + + {rank.title} + + + + {rank.description} + + + + + + )); +} + +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)); + }, +}); diff --git a/src/equicordplugins/imageLink/index.ts b/src/equicordplugins/imageLink/index.ts deleted file mode 100644 index d3cce116..00000000 --- a/src/equicordplugins/imageLink/index.ts +++ /dev/null @@ -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", - }, - }, - ], -}); diff --git a/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx b/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx new file mode 100644 index 00000000..ce5d2652 --- /dev/null +++ b/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx @@ -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 = ({ 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 ( + + + Theme Details + + + + Author + + + + + + + {author.username} + + + Source + + openModal(modalProps => ( + + + Theme Source + + + + + + + + modalProps.onClose()} + > + Close + + { + Clipboard.copy(content); + showToast("Copied to Clipboard", Toasts.Type.SUCCESS); + }}>Copy to Clipboard + + + ))} + > + View Theme Source + + + {version && ( + <> + Version + + {version} + + > + )} + {donate && ( + <> + Donate + + You can support the author by donating below. + + + VencordNative.native.openExternal(donate)}> + + Donate + + + > + )} + {theme.guild && ( + <> + Support Server + + {theme.guild.name} + + + { + 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 + + + > + )} + {theme.tags && ( + <> + Tags + + {theme.tags.map(tag => ( + + {tag} + + ))} + + > + )} + + + + + props.onClose()} + > + Close + + + + ); +}; diff --git a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx new file mode 100644 index 00000000..a3be760f --- /dev/null +++ b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx @@ -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> = 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([]); + const [filteredThemes, setFilteredThemes] = useState([]); + 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 ( + + <> + {loading ? ( + Loading Themes... + ) : (<> + + + Newest Additions + + + {themes.slice(0, 2).map((theme: Theme) => ( + + + {theme.name} + + + {theme.description} + + + + {theme.tags && ( + + {theme.tags.map(tag => ( + + {tag} + + ))} + + )} + + {themeLinks.includes("https://themes-delta.vercel.app/api/" + theme.name) ? ( + { + 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 + + ) : ( + { + 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 + + )} + { + const author = await getUser(theme.author.discord_snowflake, theme.author.discord_name); + openModal(props => ); + }} + size={Button.Sizes.MEDIUM} + color={Button.Colors.BRAND} + look={Button.Looks.FILLED} + > + Theme Info + + 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 + + + + + + ))} + + + Themes + + + + + v === searchValue.status} + closeOnSelect={true} + /> + + + + {filteredThemes.map((theme: Theme) => ( + + + {theme.name} + + + {theme.description} + + + + + {theme.tags && ( + + {theme.tags.map(tag => ( + + {tag} + + ))} + + )} + + {themeLinks.includes("https://themes-delta.vercel.app/api/" + theme.name) ? ( + { + 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 + + ) : ( + { + 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 + + )} + { + const author = await getUser(theme.author.discord_snowflake, theme.author.discord_name); + openModal(props => ); + }} + size={Button.Sizes.MEDIUM} + color={Button.Colors.BRAND} + look={Button.Looks.FILLED} + > + Theme Info + + 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 + + + + + + ))} + + >)} + > + + ); +} + +function SubmitThemes() { + const currentUser = UserStore.getCurrentUser(); + const [themeContent, setContent] = useState(""); + + const handleChange = (v: string) => setContent(v); + + return ( + + + Theme Guidelines + + + Follow the formatting for your CSS to get credit for your theme. You can find the template below. + + + (your theme will be reviewed and can take up to 24 hours to be approved) + + + + + + Submit Themes + + + + + { + 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 + + + By submitting your theme, you agree to your Discord User ID being processed. + + + + + ); +} + +function ThemeLibrary() { + const [currentTab, setCurrentTab] = useState(TabItem.THEMES); + + return ( + + + + Themes + + + Submit Theme + + + + {currentTab === TabItem.THEMES ? : } + + ); +} + +export default wrapTab(ThemeLibrary, "Theme Library"); diff --git a/src/equicordplugins/themeLibrary/components/styles.css b/src/equicordplugins/themeLibrary/components/styles.css new file mode 100644 index 00000000..b9c03b64 --- /dev/null +++ b/src/equicordplugins/themeLibrary/components/styles.css @@ -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; +} diff --git a/src/equicordplugins/themeLibrary/index.ts b/src/equicordplugins/themeLibrary/index.ts new file mode 100644 index 00000000..919bfe09 --- /dev/null +++ b/src/equicordplugins/themeLibrary/index.ts @@ -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) => 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) => any)[]; } + ).customSections; + + const i = customSettingsSections.findIndex(section => section({}).id === "ThemeSection"); + + if (i !== -1) customSettingsSections.splice(i, 1); + } +}); diff --git a/src/equicordplugins/themeLibrary/types.ts b/src/equicordplugins/themeLibrary/types.ts new file mode 100644 index 00000000..c8f6c390 --- /dev/null +++ b/src/equicordplugins/themeLibrary/types.ts @@ -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, +} diff --git a/src/plugins/imageLink/README.md b/src/plugins/imageLink/README.md deleted file mode 100644 index add65794..00000000 --- a/src/plugins/imageLink/README.md +++ /dev/null @@ -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. diff --git a/src/plugins/pauseInvitesForever/README.md b/src/plugins/pauseInvitesForever/README.md deleted file mode 100644 index 7ccf0b72..00000000 --- a/src/plugins/pauseInvitesForever/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# PauseInvitesForever - -Adds a button to the Security Actions modal to to pause invites indefinitely. - - diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 97d1bf1e..8ef56066 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -599,6 +599,10 @@ export const EquicordDevs = Object.freeze({ name: "kvba", id: 105170831130234880n, }, + Fafa: { + name: "Fafa", + id: 428188716641812481n, + }, } satisfies Record); // iife so #__PURE__ works correctly
+ By submitting your theme, you agree to your Discord User ID being processed. +