diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3815d0d4e..d9e2789d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: run: pnpm build --standalone - name: Generate plugin list - run: pnpm generatePluginJson dist/plugins.json + run: pnpm generatePluginJson dist/plugins.json dist/plugin-readmes.json - name: Clean up obsolete files run: | diff --git a/package.json b/package.json index 1ab9bb9bf..118ab7af9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.4.6", + "version": "1.4.7", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/generatePluginList.ts b/scripts/generatePluginList.ts index 2c96e61bc..b788178cd 100644 --- a/scripts/generatePluginList.ts +++ b/scripts/generatePluginList.ts @@ -165,7 +165,11 @@ async function parseFile(fileName: string) { data.target = target as any; } - return data; + let readme = ""; + try { + readme = readFileSync(join(fileName, "..", "README.md"), "utf-8"); + } catch { } + return [data, readme] as const; } throw fail("no default export called 'definePlugin' found"); @@ -194,18 +198,24 @@ function isPluginFile({ name }: { name: string; }) { (async () => { parseDevs(); - const plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir => + const plugins = [] as PluginData[]; + const readmes = {} as Record; + + await Promise.all(["src/plugins", "src/plugins/_core"].flatMap(dir => readdirSync(dir, { withFileTypes: true }) .filter(isPluginFile) - .map(async dirent => - parseFile(await getEntryPoint(dir, dirent)) - ) - ); + .map(async dirent => { + const [data, readme] = await parseFile(await getEntryPoint(dir, dirent)); + plugins.push(data); + if (readme) readmes[data.name] = readme; + }) + )); - const data = JSON.stringify(await Promise.all(plugins)); + const data = JSON.stringify(plugins); - if (process.argv.length > 2) { + if (process.argv.length > 3) { writeFileSync(process.argv[2], data); + writeFileSync(process.argv[3], JSON.stringify(readmes)); } else { console.log(data); } diff --git a/src/components/PluginSettings/ContributorModal.tsx b/src/components/PluginSettings/ContributorModal.tsx new file mode 100644 index 000000000..82c230259 --- /dev/null +++ b/src/components/PluginSettings/ContributorModal.tsx @@ -0,0 +1,113 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./contributorModal.css"; + +import { useSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { DevsById } from "@utils/constants"; +import { fetchUserProfile, getTheme, Theme } from "@utils/discord"; +import { ModalContent, ModalRoot, openModal } from "@utils/modal"; +import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; +import { User } from "discord-types/general"; + +import Plugins from "~plugins"; + +import { PluginCard } from "."; + +const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg"; +const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg"; +const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg"; +const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg"; + +const cl = classNameFactory("vc-author-modal-"); + +export function openContributorModal(user: User) { + openModal(modalProps => + + + + + + + + ); +} + +function GithubIcon() { + const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark; + return GitHub; +} + +function WebsiteIcon() { + const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark; + return Website; +} + +function ContributorModal({ user }: { user: User; }) { + useSettings(); + + const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id)); + + useEffect(() => { + if (!profile && !user.bot && user.id) + fetchUserProfile(user.id); + }, [user.id]); + + const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name; + const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name; + + const plugins = useMemo(() => { + const allPlugins = Object.values(Plugins); + const pluginsByAuthor = DevsById[user.id] + ? allPlugins.filter(p => p.authors.includes(DevsById[user.id])) + : allPlugins.filter(p => p.authors.some(a => a.name === user.username)); + + return pluginsByAuthor + .filter(p => !p.name.endsWith("API")) + .sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false)); + }, [user.id, user.username]); + + return ( + <> +
+ + {user.username} + +
+ {website && ( + + + + )} + {githubName && ( + + + + )} +
+
+ +
+ {plugins.map(p => + showToast("Restart to apply changes!")} + /> + )} +
+ + ); +} diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index f30cedeef..78f3c9db7 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -18,7 +18,6 @@ import { generateId } from "@api/Commands"; import { useSettings } from "@api/Settings"; -import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { proxyLazy } from "@utils/lazy"; @@ -28,7 +27,7 @@ import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, M import { LazyComponent } from "@utils/react"; import { OptionType, Plugin } from "@utils/types"; import { findByCode, findByPropsLazy } from "@webpack"; -import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common"; +import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common"; import { User } from "discord-types/general"; import { Constructor } from "type-fest"; @@ -41,7 +40,7 @@ import { SettingSliderComponent, SettingTextComponent } from "./components"; -import hideBotTagStyle from "./userPopoutHideBotTag.css?managed"; +import { openContributorModal } from "./ContributorModal"; const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); @@ -92,27 +91,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options)); React.useEffect(() => { - enableStyle(hideBotTagStyle); - - let originalUser: User; (async () => { for (const user of plugin.authors.slice(0, 6)) { const author = user.id ? await UserUtils.fetchUser(`${user.id}`) - // only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc) - .then(u => (originalUser = u, makeDummyUser(u))) .catch(() => makeDummyUser({ username: user.name })) : makeDummyUser({ username: user.name }); setAuthors(a => [...a, author]); } })(); - - return () => { - disableStyle(hideBotTagStyle); - if (originalUser) - FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser }); - }; }, []); async function saveAndClose() { @@ -214,6 +202,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti showDefaultAvatarsForNullUsers showUserPopout renderMoreUsers={renderMoreUsers} + renderUser={(user: User) => ( + openContributorModal(user)} + > + {user.username} + + )} /> diff --git a/src/components/PluginSettings/contributorModal.css b/src/components/PluginSettings/contributorModal.css new file mode 100644 index 000000000..a8af8c8b8 --- /dev/null +++ b/src/components/PluginSettings/contributorModal.css @@ -0,0 +1,57 @@ +.vc-author-modal-root { + padding: 1em; +} + +.vc-author-modal-header { + display: flex; + align-items: center; + margin-bottom: 1em; +} + +.vc-author-modal-name { + text-transform: none; + flex-grow: 0; + background: var(--background-tertiary); + border-radius: 0 9999px 9999px 0; + padding: 6px 0.8em 6px 0.5em; + font-size: 20px; + height: 20px; + position: relative; +} + +.vc-author-modal-name::before { + content: ""; + display: block; + position: absolute; + height: 100%; + width: 16px; + background: var(--background-tertiary); + z-index: -1; + left: -16px; + top: 0; +} + +.vc-author-modal-avatar { + height: 32px; + width: 32px; + border-radius: 50%; +} + +.vc-author-modal-links { + margin-left: auto; + display: flex; + gap: 0.2em; +} + +.vc-author-modal-links img { + height: 32px; + width: 32px; + border-radius: 50%; + border: 4px solid var(--background-tertiary); + box-sizing: border-box +} + +.vc-author-modal-plugins { + display: grid; + gap: 0.5em; +} diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index f19d32647..3712f3de1 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -91,7 +91,7 @@ interface PluginCardProps extends React.HTMLProps { isNew?: boolean; } -function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) { +export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) { const settings = Settings.plugins[plugin.name]; const isEnabled = () => settings.enabled ?? false; diff --git a/src/components/PluginSettings/userPopoutHideBotTag.css b/src/components/PluginSettings/userPopoutHideBotTag.css deleted file mode 100644 index 5e33e4b32..000000000 --- a/src/components/PluginSettings/userPopoutHideBotTag.css +++ /dev/null @@ -1,3 +0,0 @@ -[class|="userPopoutOuter"] [class*="botTag"] { - display: none; -} diff --git a/src/components/VencordSettings/addonCard.css b/src/components/VencordSettings/addonCard.css index 92f8c2578..f2dee11d9 100644 --- a/src/components/VencordSettings/addonCard.css +++ b/src/components/VencordSettings/addonCard.css @@ -8,6 +8,7 @@ width: 100%; transition: 0.1s ease-out; transition-property: box-shadow, transform, background, opacity; + box-sizing: border-box; } .vc-addon-card-disabled { diff --git a/src/plugins/blurNsfw.ts b/src/plugins/blurNsfw.ts index dadc49be5..54b1e49a4 100644 --- a/src/plugins/blurNsfw.ts +++ b/src/plugins/blurNsfw.ts @@ -49,7 +49,7 @@ export default definePlugin({ replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" }, { match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g, - replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" + replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')" }] } ], diff --git a/src/plugins/lastfm.tsx b/src/plugins/lastfm.tsx index a55f46156..66be06aa6 100644 --- a/src/plugins/lastfm.tsx +++ b/src/plugins/lastfm.tsx @@ -72,6 +72,12 @@ const enum ActivityFlag { INSTANCE = 1 << 0, } +const enum NameFormat { + StatusName = "status-name", + ArtistFirst = "artist-first", + SongFirst = "song-first", +} + const applicationId = "1108588077900898414"; const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; @@ -117,10 +123,29 @@ const settings = definePluginSettings({ default: true, }, statusName: { - description: "text shown in status", + description: "custom status text", type: OptionType.STRING, default: "some music", }, + nameFormat: { + description: "Show name of song and artist in status name", + type: OptionType.SELECT, + options: [ + { + label: "Use custom status name", + value: NameFormat.StatusName, + default: true + }, + { + label: "Use format 'artist - song'", + value: NameFormat.ArtistFirst + }, + { + label: "Use format 'song - artist'", + value: NameFormat.SongFirst + } + ], + }, useListeningStatus: { description: 'show "Listening to" status instead of "Playing"', type: OptionType.BOOLEAN, @@ -140,13 +165,13 @@ const settings = definePluginSettings({ value: "placeholder" } ], - } + }, }); export default definePlugin({ name: "LastFMRichPresence", description: "Little plugin for Last.fm rich presence", - authors: [Devs.dzshn, Devs.RuiNtD], + authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip], settingsAboutComponent: () => ( <> @@ -267,9 +292,20 @@ export default definePlugin({ url: `https://www.last.fm/user/${settings.store.username}`, }); + const statusName = (() => { + switch (settings.store.nameFormat) { + case NameFormat.ArtistFirst: + return trackData.artist + " - " + trackData.name; + case NameFormat.SongFirst: + return trackData.name + " - " + trackData.artist; + default: + return settings.store.statusName; + } + })(); + return { application_id: applicationId, - name: settings.store.statusName, + name: statusName, details: trackData.name, state: trackData.artist, diff --git a/src/plugins/pictureInPicture.tsx b/src/plugins/pictureInPicture.tsx new file mode 100644 index 000000000..7a9e7903f --- /dev/null +++ b/src/plugins/pictureInPicture.tsx @@ -0,0 +1,83 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { React, Tooltip } from "@webpack/common"; + +const settings = definePluginSettings({ + loop: { + description: "Whether to make the PiP video loop or not", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: false + } +}); + +export default definePlugin({ + name: "PictureInPicture", + description: "Adds picture in picture to videos (next to the Download button)", + authors: [Devs.Lumap], + settings, + + patches: [ + { + find: ".onRemoveAttachment,", + replacement: { + match: /\.nonMediaAttachment.{0,10}children:\[(\i),/, + replace: "$&$1&&$self.renderPiPButton()," + }, + }, + ], + + renderPiPButton: ErrorBoundary.wrap(() => { + return ( + + {tooltipProps => ( +
{ + const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!; + const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement; + + videoClone.loop = settings.store.loop; + videoClone.style.display = "none"; + videoClone.onleavepictureinpicture = () => videoClone.remove(); + + function launchPiP() { + videoClone.currentTime = video.currentTime; + videoClone.requestPictureInPicture(); + video.pause(); + videoClone.play(); + } + + if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */) + launchPiP(); + else + videoClone.onloadedmetadata = launchPiP; + }} + > + + + +
+ )} +
+ ); + }, { noop: true }) +}); diff --git a/src/plugins/pronoundb/pronoundbUtils.ts b/src/plugins/pronoundb/pronoundbUtils.ts index 8bde10e9a..eac204b7d 100644 --- a/src/plugins/pronoundb/pronoundbUtils.ts +++ b/src/plugins/pronoundb/pronoundbUtils.ts @@ -21,14 +21,11 @@ import { VENCORD_USER_AGENT } from "@utils/constants"; import { debounce } from "@utils/debounce"; import { getCurrentChannel } from "@utils/discord"; import { useAwaiter } from "@utils/react"; -import { findStoreLazy } from "@webpack"; -import { UserStore } from "@webpack/common"; +import { UserProfileStore, UserStore } from "@webpack/common"; import { settings } from "./settings"; import { PronounCode, PronounMapping, PronounsResponse } from "./types"; -const UserProfileStore = findStoreLazy("UserProfileStore"); - type PronounsWithSource = [string | null, string]; const EmptyPronouns: PronounsWithSource = [null, ""]; diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx index 404a8db1b..1f6ef34ef 100644 --- a/src/plugins/showConnections/index.tsx +++ b/src/plugins/showConnections/index.tsx @@ -27,13 +27,12 @@ import { copyWithToast } from "@utils/misc"; import { LazyComponent } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack"; -import { Text, Tooltip } from "@webpack/common"; +import { Text, Tooltip, UserProfileStore } from "@webpack/common"; import { User } from "discord-types/general"; import { VerifiedIcon } from "./VerifiedIcon"; const Section = LazyComponent(() => findByCode("().lastSection")); -const UserProfileStore = findStoreLazy("UserProfileStore"); const ThemeStore = findStoreLazy("ThemeStore"); const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl"); const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"'); diff --git a/src/plugins/supportHelper.tsx b/src/plugins/supportHelper.tsx index 4691c0e08..f36cc0fdb 100644 --- a/src/plugins/supportHelper.tsx +++ b/src/plugins/supportHelper.tsx @@ -48,7 +48,7 @@ export default definePlugin({ name: "vencord-debug", description: "Send Vencord Debug info", predicate: ctx => AllowedChannelIds.includes(ctx.channel.id), - execute() { + async execute() { const { RELEASE_CHANNEL } = window.GLOBAL_ENV; const client = (() => { @@ -75,6 +75,10 @@ export default definePlugin({ OpenAsar: "openasar" in window, }; + if (IS_DISCORD_DESKTOP) { + info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A"; + } + const debugInfo = ` **Vencord Debug Info** >>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 245c8bbb1..424c9aae4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -355,6 +355,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "bb010g", id: 72791153467990016n, }, + Lumap: { + name: "lumap", + id: 635383782576357407n + }, Dolfies: { name: "Dolfies", id: 852892297661906993n, @@ -363,6 +367,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "RuukuLada", id: 119705748346241027n, }, + blahajZip: { + name: "blahaj.zip", + id: 683954422241427471n, + } } satisfies Record); // iife so #__PURE__ works correctly diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index 4f4326b65..458509b42 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -18,7 +18,7 @@ import { MessageObject } from "@api/MessageEvents"; import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack"; -import { ChannelStore, ComponentDispatch, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, SelectedChannelStore, SelectedGuildStore, UserUtils } from "@webpack/common"; +import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileStore, UserUtils } from "@webpack/common"; import { Guild, Message, User } from "discord-types/general"; import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal"; @@ -118,6 +118,41 @@ export async function openUserProfile(id: string) { }); } +interface FetchUserProfileOptions { + friend_token?: string; + connections_role_id?: string; + guild_id?: string; + with_mutual_guilds?: boolean; + with_mutual_friends_count?: boolean; +} + +/** + * Fetch a user's profile + */ +export async function fetchUserProfile(id: string, options?: FetchUserProfileOptions) { + const cached = UserProfileStore.getUserProfile(id); + if (cached) return cached; + + FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id }); + + const { body } = await RestAPI.get({ + url: `/users/${id}/profile`, + query: { + with_mutual_guilds: false, + with_mutual_friends_count: false, + ...options + }, + oldFormErrors: true, + }); + + FluxDispatcher.dispatch({ type: "USER_UPDATE", user: body.user }); + await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", ...body }); + if (options?.guild_id && body.guild_member) + FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member }); + + return UserProfileStore.getUserProfile(id); +} + /** * Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise */ diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 456255d30..d42cb6b56 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -48,6 +48,7 @@ export let PoggerModeSettingsStore: GenericStore; export let GuildStore: Stores.GuildStore & t.FluxStore; export let UserStore: Stores.UserStore & t.FluxStore; +export let UserProfileStore: GenericStore; export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore; export let SelectedGuildStore: t.FluxStore & Record; export let ChannelStore: Stores.ChannelStore & t.FluxStore; @@ -86,6 +87,7 @@ export const useStateFromStores: ( waitForStore("DraftStore", s => DraftStore = s); waitForStore("UserStore", s => UserStore = s); +waitForStore("UserProfileStore", m => UserProfileStore = m); waitForStore("ChannelStore", m => ChannelStore = m); waitForStore("SelectedChannelStore", m => SelectedChannelStore = m); waitForStore("SelectedGuildStore", m => SelectedGuildStore = m); diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index 3101b2859..a3e76b273 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -398,12 +398,18 @@ export type Paginator = ComponentType<{ hideMaxPage?: boolean; }>; -export type MaskedLink = ComponentType<{ - onClick(): void; - trusted: boolean; - title: string, +export type MaskedLink = ComponentType; + rel?: string; + target?: string; + title?: string, + className?: string; + tabIndex?: number; + onClick?(): void; + trusted?: boolean; + messageId?: string; + channelId?: string; +}>>; export type ScrollerThin = ComponentType