diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cc6ef1f9..5e4a15145 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,7 +41,8 @@ jobs: cd dist mkdir release cp browser/browser.* release - cp Vencord.user.{js,js.LEGAL.txt} release + cp Equicord.user.{js,js.LEGAL.txt} release + # copy the plugin data jsons, the extension zips and the desktop/vesktop asars cp *.{json,zip,asar} release # legacy un-asared files diff --git a/.github/workflows/nixosBuild.yml b/.github/workflows/nixosBuild.yml index d99577ec0..e4cb7a88c 100644 --- a/.github/workflows/nixosBuild.yml +++ b/.github/workflows/nixosBuild.yml @@ -39,7 +39,8 @@ jobs: cd dist mkdir release cp browser/browser.* release - cp Vencord.user.{js,js.LEGAL.txt} release + cp Equicord.user.{js,js.LEGAL.txt} release + # copy the plugin data jsons, the extension zips and the desktop/vesktop asars cp *.{json,zip,asar} release # legacy un-asared files diff --git a/browser/content.js b/browser/content.js index 2b51ebabf..cd1f59793 100644 --- a/browser/content.js +++ b/browser/content.js @@ -5,7 +5,7 @@ if (typeof browser === "undefined") { const style = document.createElement("link"); style.type = "text/css"; style.rel = "stylesheet"; -style.href = browser.runtime.getURL("dist/Vencord.css"); +style.href = browser.runtime.getURL("dist/Equicord.css"); document.addEventListener( "DOMContentLoaded", diff --git a/browser/manifest.json b/browser/manifest.json index 96fcdc571..cb594c1a7 100644 --- a/browser/manifest.json +++ b/browser/manifest.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "111", "name": "Equicord Web", "description": "The other cutest Discord mod now in your browser", - "author": "Thororen", + "author": "Equicord", "homepage_url": "https://github.com/Equicord/Equicord", "icons": { "128": "icon.png" @@ -33,7 +33,7 @@ "*://*.discord.com/*" ], "js": [ - "dist/Vencord.js" + "dist/Equicord.js" ], "all_frames": true, "world": "MAIN" diff --git a/browser/manifestv2.json b/browser/manifestv2.json index 172a42e50..b95080d54 100644 --- a/browser/manifestv2.json +++ b/browser/manifestv2.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "91", "name": "Equicord Web", "description": "The other cutest Discord mod now in your browser", - "author": "Thororen", + "author": "Equicord", "homepage_url": "https://github.com/Equicord/Equicord", "icons": { "128": "icon.png" @@ -32,7 +32,7 @@ "*://*.discord.com/*" ], "js": [ - "dist/Vencord.js" + "dist/Equicord.js" ], "all_frames": true, "world": "MAIN" @@ -44,13 +44,13 @@ ] }, "web_accessible_resources": [ - "dist/Vencord.js", - "dist/Vencord.css" + "dist/Equicord.js", + "dist/Equicord.css" ], "browser_specific_settings": { "gecko": { - "id": "vencord-firefox@vendicated.dev", + "id": "equicord-firefox@equicord.org", "strict_min_version": "128.0" } } -} \ No newline at end of file +} diff --git a/packages/discord-types/src/common/Guild.d.ts b/packages/discord-types/src/common/Guild.d.ts index 5b5c3c745..65e973ffe 100644 --- a/packages/discord-types/src/common/Guild.d.ts +++ b/packages/discord-types/src/common/Guild.d.ts @@ -47,7 +47,7 @@ export class Guild extends DiscordRecord { }; premiumProgressBarEnabled: boolean; premiumSubscriberCount: number; - premiumTier: number; + premiumTier: 0 | 1 | 2 | 3; profile: { badge: string | undefined; tag: string | undefined; diff --git a/packages/discord-types/src/stores/MediaEngineStore.d.ts b/packages/discord-types/src/stores/MediaEngineStore.d.ts index d44f5aa58..4ef81639d 100644 --- a/packages/discord-types/src/stores/MediaEngineStore.d.ts +++ b/packages/discord-types/src/stores/MediaEngineStore.d.ts @@ -1,10 +1,37 @@ import { FluxStore } from ".."; export class MediaEngineStore extends FluxStore { - isSelfMute(): boolean; - isSelfDeaf(): boolean; + isAdvancedVoiceActivitySupported(): boolean; + isAecDumpSupported(): boolean; + isAnyLocalVideoAutoDisabled(): boolean; + isAutomaticGainControlSupported(): boolean; + isDeaf(): boolean; + isEnableHardwareMuteNotice(): boolean; + isEnabled(): boolean; + isExperimentalEncodersSupported(): boolean; + isHardwareMute(): boolean; + isInputProfileCustom(): boolean; + isInteractionRequired(): boolean; + isLocalMute(): boolean; isLocalMute(userId): boolean; + isLocalVideoAutoDisabled(userId): boolean; isLocalVideoDisabled(userId): boolean; + isLocalVideoDisabled(): boolean; + isMediaFilterSettingLoading(): boolean; + isMute(): boolean; + isNativeAudioPermissionReady(): boolean; + isNoiseCancellationError(): boolean; + isNoiseCancellationSupported(): boolean; + isNoiseSuppressionSupported(): boolean; + isScreenSharing(): boolean; + isSelfDeaf(): boolean; + isSelfMute(): boolean; + isSelfMutedTemporarily(): boolean; + isSimulcastSupported(): boolean; + isSoundSharing(): boolean; + isSupported(): boolean; + isVideoAvailable(): boolean; + isVideoEnabled(): boolean; setAv1Enabled(AV1: boolean): void; setH265Enabled(H265: boolean): void; diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 86eec848d..7d7d5518e 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -105,7 +105,7 @@ const buildConfigs = [ IS_USERSCRIPT: "true", window: "unsafeWindow", }, - outfile: "dist/Vencord.user.js", + outfile: "dist/Equicord.user.js", banner: { js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${VERSION}.${new Date().getTime()}`) }, @@ -155,8 +155,8 @@ async function loadDir(dir, basePath = "") { */ async function buildExtension(target, files) { const entries = { - "dist/Vencord.js": await readFile("dist/browser/extension.js"), - "dist/Vencord.css": await readFile("dist/browser/extension.css"), + "dist/Equicord.js": await readFile("dist/browser/extension.js"), + "dist/Equicord.css": await readFile("dist/browser/extension.css"), ...await loadDir("dist/browser/vendor/monaco", "dist/browser/"), ...Object.fromEntries(await Promise.all(files.map(async f => { let content = await readFile(join("browser", f)); @@ -184,7 +184,7 @@ async function buildExtension(target, files) { console.info("Unpacked Extension written to dist/browser/" + target); } -const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => { +const appendCssRuntime = readFile("dist/Equicord.user.css", "utf-8").then(content => { const cssRuntime = ` ;document.addEventListener("DOMContentLoaded", () => document.body.insertAdjacentElement("afterend", Object.assign(document.createElement("style"), { @@ -194,7 +194,7 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content ), { once: true }); `; - return appendFile("dist/Vencord.user.js", cssRuntime); + return appendFile("dist/Equicord.user.js", cssRuntime); }); if (!process.argv.includes("--skip-extension")) { diff --git a/src/components/settings/tabs/changelog/index.tsx b/src/components/settings/tabs/changelog/index.tsx index d349178c6..ac24023b0 100644 --- a/src/components/settings/tabs/changelog/index.tsx +++ b/src/components/settings/tabs/changelog/index.tsx @@ -13,13 +13,15 @@ import { DeleteIcon } from "@components/Icons"; import { Link } from "@components/Link"; import { Paragraph } from "@components/Paragraph"; import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab"; +import { gitHashShort } from "@shared/vencordUserAgent"; import { Margins } from "@utils/margins"; import { useAwaiter } from "@utils/react"; -import { getRepo, shortGitHash, UpdateLogger } from "@utils/updater"; +import { getRepo, UpdateLogger } from "@utils/updater"; import { Alerts, Button, Card, React, Toasts } from "@webpack/common"; import gitHash from "~git-hash"; +import { HashLink } from "../updater/Components"; import { ChangelogEntry, ChangelogHistory, @@ -40,22 +42,6 @@ import { } from "./changelogManager"; import { NewPluginsCompact, NewPluginsSection } from "./NewPluginsSection"; -function HashLink({ - repo, - hash, - disabled = false, -}: { - repo: string; - hash: string; - disabled?: boolean; -}) { - return ( - - {hash} - - ); -} - function ChangelogCard({ entry, repo, @@ -78,7 +64,7 @@ function ChangelogCard({ @@ -131,7 +117,7 @@ function UpdateLogCard({ @@ -128,7 +128,7 @@ function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: { >Save @@ -156,7 +156,7 @@ function DeleteFolderConfirmationModal({ modalProps, modalKey, onConfirm }) { - - - - - - ); -} - -export const settings = definePluginSettings({ - displayStatus: { - description: "Display a status notification when downloads start, finish, or error.", - type: OptionType.BOOLEAN, - default: true, - }, - statusDuration: { - type: OptionType.SLIDER, - description: "The number of seconds to display status notifications.", - markers: [1, 3, 5], - default: 2.5, - stickToMarkers: false, - }, - voiceMessages: { - description: "Add a download button to voice messages.", - type: OptionType.BOOLEAN, - default: true, - }, - allowUnicode: { - description: "Allow non-ASCII characters in file names.", - type: OptionType.BOOLEAN, - default: true, - }, - overwriteFiles: { - description: "If a default directory is set and a download file name matches an existing file, overwrite the file. If disabled, a number will be appended to the file name.", - type: OptionType.BOOLEAN, - default: false, - }, - defaultDirectory: { - component: DefaultDirectorySetting, - type: OptionType.COMPONENT, - default: "", - }, -}); diff --git a/src/equicordplugins/downloadify.desktop/style.css b/src/equicordplugins/downloadify.desktop/style.css deleted file mode 100644 index 2b7f3df5e..000000000 --- a/src/equicordplugins/downloadify.desktop/style.css +++ /dev/null @@ -1,77 +0,0 @@ -.downloadify-voice-message-container { - flex-shrink: 0; - align-items: center; - display: flex; - flex-direction: column; - justify-content: flex-end; - position: relative; - margin: 0 -2px; -} - -.downloadify-voice-message-button { - cursor: pointer; - line-height: 0; - border: none; - width: auto; - background: transparent; - color: var(--interactive-normal); - margin: 0; - padding: 0; - align-items: center; - border-radius: 8px; - box-sizing: border-box; - display: flex; - font-size: 14px; - font-weight: var(--font-weight-medium); - justify-content: center; - position: relative; - transition-duration: .2s; - user-select: none; -} - -.downloadify-voice-message-button:hover { - color: var(--interactive-hover); -} - -.downloadify-directory-container { - margin-top: 10px; -} - -.downloadify-directory-buttons { - margin-top: 10px; - display: flex; - gap: 10px; -} - -.downloadify-directory-button { - border-radius: 4px; - min-height: 34px; - color: white; - height: 34px; - flex: 1; -} - -.downloadify-directory-button:not(.downloadify-disabled):hover { - filter: brightness(0.75); - transition: filter 0.15s ease-in-out; -} - -.downloadify-clear-button { - background-color: rgb(180 71 78); -} - -.downloadify-browse-button { - background-color: rgb(87 100 240); -} - -.downloadify-directory-display { - background-color: var(--background-base-lower); - overflow-wrap: break-word; - font-family: monospace; - word-wrap: break-word; - white-space: pre-wrap; - border-radius: 4px; - user-select: text; - font-size: 14px; - padding: 8px; -} \ No newline at end of file diff --git a/src/equicordplugins/downloadify.desktop/utils/definitions.tsx b/src/equicordplugins/downloadify.desktop/utils/definitions.tsx deleted file mode 100644 index 8f65a964a..000000000 --- a/src/equicordplugins/downloadify.desktop/utils/definitions.tsx +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { classNameFactory } from "@api/Styles"; -import { Logger } from "@utils/Logger"; -import { PluginNative } from "@utils/types"; -import { Channel, Guild, GuildMember, Message, User } from "@vencord/discord-types"; -import { findByPropsLazy } from "@webpack"; -import { JSX } from "react"; - -export const d = classNameFactory("downloadify-"); -export const CDN_BASE = "https://cdn.discordapp.com"; -export const MEDIA_PROXY_BASE = "https://media.discordapp.net"; -export const PRIMARY_DOMAIN_BASE = "https://discord.com"; -export const IMAGE_EXT_1_DOMAIN_BASE = "https://images-ext-1.discordapp.net"; -export const IMAGE_EXT_2_DOMAIN_BASE = "https://images-ext-2.discordapp.net"; -export const TENOR_BASE = "https://c.tenor.com"; -export const TENOR_GIF_ID = "Ad"; -export const TENOR_MP4_ID = "Po"; -export const ASSET_TYPE_EXTRACTOR = new RegExp(/^https:\/\/(?:cdn\.discordapp\.com|media.discordapp.net)\/([a-z-]+(?:\/[a-z-]+){0,2})(?:(?:\/\d+\/[a-z-]+\/\d+\/([a-z-]+))|(?:\/([a-z-]+(?:\/[a-z-_]+)?)))?/i); -export const DownloadifyLogger = new Logger("Downloadify"); -export const DownloadifyNative = VencordNative.pluginHelpers.Downloadify as PluginNative; -export const defaultAssets = findByPropsLazy("DEFAULT_GROUP_DM_AVATARS") as DefaultAssets; - -interface DefaultAssets { - BOT_AVATARS: Record; - DEFAULT_AVATARS: string[]; - DEFAULT_CHANNEL_ICON: string; - DEFAULT_GROUP_DM_AVATARS: string[]; - DEFAULT_PROVISIONAL_AVATARS: string[]; -} - -export interface AssetInfo { - source: AssetSource; - static: string[]; - animated: string[]; - type?: string; - forceAnimated?: boolean; -} - -export enum AssetSource { - CDN = "cdn", - EXTERNAL_IMAGE_PROXY = "external_image_proxy", - ATTACHMENT_MEDIA_PROXY = "attachment_media_proxy", - ASSET_MEDIA_PROXY = "asset_media_proxy", - PRIMARY_DOMAIN = "primary_domain", - EXTERNAL = "external", - TENOR = "tenor" -} - -export const unknownExternal = { - source: AssetSource.EXTERNAL, - static: [], - animated: [] -}; - -export const unknownPrimaryDomain = { - source: AssetSource.PRIMARY_DOMAIN, - static: [], - animated: [] -}; - -export const unknownCDN = { - source: AssetSource.CDN, - static: [], - animated: [] -}; - -export const unknownAssetMediaProxy = { - source: AssetSource.ASSET_MEDIA_PROXY, - static: [], - animated: [] -}; - -export const unknownAttachmentMediaProxy = { - source: AssetSource.ATTACHMENT_MEDIA_PROXY, - static: [], - animated: [] -}; - -export const unknownExternalImageProxy = { - source: AssetSource.EXTERNAL_IMAGE_PROXY, - static: [], - animated: [] -}; - -export const defaultResourceAvailability = { - source: AssetSource.PRIMARY_DOMAIN, - static: ["png"], - animated: [] -}; - -export const APNGAvailability = { - source: AssetSource.ASSET_MEDIA_PROXY, - static: ["png", "webp", "jpg"], - animated: ["apng", "png", "webp", "jpg"] -}; - -export const hashableAvailability = { - source: AssetSource.ASSET_MEDIA_PROXY, - static: ["png", "webp", "jpg"], - animated: ["gif", "awebp", "png", "webp", "jpg"] -}; - -// Defines known asset type availability. Unknown types which may be -// encountered are AssetSource.EXTERNAL which are present in some embeds, -// as well as OTHER attachment types. -export const assetAvailability = { - "user-avatar": hashableAvailability, - "user-banner": hashableAvailability, - "avatar-decoration": APNGAvailability, - "clan-badge": { - source: AssetSource.ASSET_MEDIA_PROXY, - static: ["png"], - animated: [] - }, - "nameplate": { - source: AssetSource.CDN, - static: ["png"], - animated: ["webm", "apng", "png"], - type: "nameplate" - }, - "default-user-avatar": defaultResourceAvailability, - "default-group-icon": defaultResourceAvailability, - "role-icon-custom": { - source: AssetSource.ASSET_MEDIA_PROXY, - static: ["png", "webp", "jpg"], - animated: [], - }, - "role-icon-unicode-emoji": { - source: AssetSource.PRIMARY_DOMAIN, - static: ["svg"], - animated: [], - }, - "guild-icon": hashableAvailability, - "guild-banner": hashableAvailability, - "guild-invite-splash": hashableAvailability, - "guild-discovery-splash": hashableAvailability, - "custom-emoji": hashableAvailability, - "unicode-emoji": { - source: AssetSource.PRIMARY_DOMAIN, - static: ["svg"], - animated: [] - }, - "sticker": { - "APNG": APNGAvailability, - "PNG": { - source: AssetSource.ASSET_MEDIA_PROXY, - static: ["png", "webp", "jpg"], - animated: [] - }, - "GIF": { - source: AssetSource.ASSET_MEDIA_PROXY, - static: [], - animated: ["gif", "awebp", "png", "webp", "jpg"], - forceAnimated: true - }, - "LOTTIE": { - source: AssetSource.CDN, - static: ["json"], - animated: ["json"] - }, - }, - "tenor": { - source: AssetSource.TENOR, - static: [], - animated: ["gif", "mp4"], - forceAnimated: true - }, - "external": { - "image/png": { - source: AssetSource.EXTERNAL_IMAGE_PROXY, - static: ["png", "webp", "jpg"], - animated: [] - }, - "image/webp": { - source: AssetSource.EXTERNAL_IMAGE_PROXY, - static: ["png", "webp", "jpg"], - animated: [] - }, - "image/jpeg": { - source: AssetSource.EXTERNAL_IMAGE_PROXY, - static: ["png", "webp", "jpg"], - animated: [] - }, - "video/mp4": { - source: AssetSource.EXTERNAL, - static: [], - animated: ["mp4"], - forceAnimated: true - }, - }, - "attachment": { - // Image attachments over a certain size and/or resolution are treated as - // non-image files and are therefore not available in alternative formats. - // This is designated by an attachment type of OTHER instead of IMAGE. - "image/png": { - source: AssetSource.ATTACHMENT_MEDIA_PROXY, - static: ["png", "webp", "jpg"], - // APNG attachments are technically supported, but appear to get - // broken by Discord squashing them into static PNGs. APNG is kept - // here in case it is fixed some day. - animated: ["apng", "png", "webp", "jpg"] - }, - "image/webp": { - source: AssetSource.ATTACHMENT_MEDIA_PROXY, - static: ["webp", "png", "jpg"], - animated: ["awebp", "webp", "png", "jpg"] - }, - "image/avif": { - source: AssetSource.ATTACHMENT_MEDIA_PROXY, - static: ["avif", "png", "webp"], - animated: ["avif", "awebp", "png", "webp"] - }, - "image/jpeg": { - source: AssetSource.ATTACHMENT_MEDIA_PROXY, - static: ["jpg", "png", "webp"], - animated: [] - }, - "image/gif": { - source: AssetSource.ATTACHMENT_MEDIA_PROXY, - static: [], - animated: ["gif", "awebp", "png", "webp", "jpg"], - forceAnimated: true - }, - } -}; - -export function DownloadIcon({ width, height }: { width: number; height: number; }): JSX.Element { - return ( - - - - ); -} - -export interface ParsedFile { - path: string; - baseName: string; - extension: string | null; - twitterExtra: string | null; -} - -export interface ParsedURL { - url: string; - host: string; - path: string; - baseName: string; - extension: string | null; - params: { expiry: Record, other: Record; }; - twitterExtra: string | null; -} - -export interface InsertGroup { - id: { group?: string, child?: string; }; - type: "WITH_GROUP" | "BEFORE_GROUP" | "AFTER_GROUP"; - position?: "START" | "END"; -} - -export interface VoiceMessageDownloadButtonProps { - message: Message; - item: { - type: "AUDIO"; - originalItem: { - filename: string; - proxy_url: string; - }; - }; -} - -export interface HoverDownloadProps { - item: { - type: "AUDIO" | "IMAGE" | "VIDEO" | "OTHER"; - downloadUrl: string; - contentType: string; - srcIsAnimated: boolean; - originalItem: { - title?: string; - filename: string; - proxy_url: string; - url: string; - }; - }; -} - -export interface ExpandedModalDownloadProps { - item: { - type: "AUDIO" | "IMAGE" | "VIDEO" | "OTHER"; - url: string; - original: string; - proxyUrl?: string; - contentType?: string; - srcIsAnimated: boolean; - sourceMetadata?: { - identifier: { - type: "attachment" | "embed"; - title?: string; - }, - }; - }; -} - -export interface MessageContextMenuProps { - message: Message; - channel: Channel; - itemSrc: string; - itemSafeSrc?: string; - favoriteableType: "sticker" | "emoji" | null; - favoriteableName: string | null; - favoriteableId: string | null; - contextMenuAPIArguments: any[]; - mediaItem?: { - contentType: string; - proxyUrl: string; - url: string; - }; -} - -export interface GDMContextMenuProps { - channel: Channel & { - icon: string | null; - }; -} - -export interface GuildContextMenuProps { - guild: Guild; -} - -export interface UserContextMenuProps { - user: User; - guildId?: string; -} - -export interface CustomizedUser extends User { - avatarDecorationData: null | { - asset: string; - }; - collectibles: null | Record<"nameplate", { - asset: string; - label: string; - palette: string; - }>; -} - -export interface CustomizedMember extends Omit { - avatarDecoration: null | { - asset: string; - }; -} - -export const AttachmentFlags = { - IS_CLIP: 1 << 0, - IS_THUMBNAIL: 1 << 1, - IS_REMIX: 1 << 2, - IS_SPOILER: 1 << 3, - CONTAINS_EXPLICIT_MEDIA: 1 << 4, - IS_ANIMATED: 1 << 5, - CONTAINS_GORE_CONTENT: 1 << 6 -}; diff --git a/src/equicordplugins/downloadify.desktop/utils/handlers.tsx b/src/equicordplugins/downloadify.desktop/utils/handlers.tsx deleted file mode 100644 index 43f336578..000000000 --- a/src/equicordplugins/downloadify.desktop/utils/handlers.tsx +++ /dev/null @@ -1,999 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { ImageIcon } from "@components/Icons"; -import { StickerFormatType } from "@vencord/discord-types/enums"; -import { GuildMemberStore, GuildRoleStore, GuildStore, Menu, showToast, Toasts, UserProfileStore, UserStore } from "@webpack/common"; -import { JSX } from "react"; - -import { settings } from "../settings"; -import { ASSET_TYPE_EXTRACTOR, assetAvailability, AssetInfo, AssetSource, AttachmentFlags, CDN_BASE, CustomizedMember, CustomizedUser, d, defaultAssets, DownloadIcon, DownloadifyLogger, DownloadifyNative, ExpandedModalDownloadProps, GDMContextMenuProps, GuildContextMenuProps, HoverDownloadProps, IMAGE_EXT_1_DOMAIN_BASE, IMAGE_EXT_2_DOMAIN_BASE, MEDIA_PROXY_BASE, MessageContextMenuProps, ParsedURL, PRIMARY_DOMAIN_BASE, TENOR_BASE, TENOR_GIF_ID, TENOR_MP4_ID, unknownAttachmentMediaProxy, unknownCDN, unknownExternal, unknownExternalImageProxy, unknownPrimaryDomain, UserContextMenuProps, VoiceMessageDownloadButtonProps } from "./definitions"; -import { getFormattedNow, joinOrCreateContextMenuGroup, parseFile, parseURL, sanitizeFilename } from "./misc"; - -export function MessageContextMenu(children: Array, props: MessageContextMenuProps): void { - if (!children?.length || !props?.message?.id) { - return; - } - - const { allowUnicode } = settings.store; - let { favoriteableId, favoriteableName, favoriteableType } = props; - const { itemSrc, itemSafeSrc, message, channel, mediaItem, contextMenuAPIArguments } = props; - const target = contextMenuAPIArguments?.[0]?.target; - let targetURL = itemSafeSrc ?? itemSrc; - - const emojiTarget = target.dataset?.type === "emoji" - ? target - : target.children?.[1]?.firstChild?.dataset?.type === "emoji" - ? target.children[1].firstChild - : target.parentElement?.children?.[1]?.firstChild?.dataset?.type === "emoji" - ? target.parentElement.children[1].firstChild - : null; - - if (!!emojiTarget?.dataset?.id) { - favoriteableType = "emoji"; - favoriteableId = emojiTarget.dataset.id; - targetURL = emojiTarget.src; - } else if (!!emojiTarget?.dataset?.name) { - favoriteableType = "emoji"; - favoriteableName = emojiTarget.alt ?? emojiTarget.dataset.name; - favoriteableId = null; - targetURL = emojiTarget.src; - } - - const roleIconTarget = target.classList && (Array.from(target.classList) as string[]).some(cls => cls.includes("roleIcon")) - ? target - : target.firstChild?.classList && (Array.from(target.firstChild.classList) as string[]).some(cls => cls.includes("roleIcon")) - ? target.firstChild - : null; - - const clanBadgeTarget = target.classList && (Array.from(target.classList) as string[]).some(cls => cls.includes("badge")) - ? target - : target.parentElement?.firstChild?.classList && (Array.from(target.parentElement.firstChild.classList) as string[]).some(cls => cls.includes("badge")) - ? target.parentElement.firstChild - : target.firstChild?.firstChild?.classList && (Array.from(target.firstChild.firstChild.classList) as string[]).some(cls => cls.includes("badge")) - ? target.firstChild.firstChild - : null; - - if (target.tagName === "VIDEO" && !!target.src) { - targetURL = contextMenuAPIArguments[0].target.src; - } else if (target.parentElement?.parentElement?.firstChild?.tagName === "VIDEO" && !!target.parentElement?.parentElement?.firstChild?.src) { - targetURL = contextMenuAPIArguments[0].target.parentElement.parentElement.firstChild.src; - } else if (target.parentElement?.parentElement?.parentElement?.firstChild?.tagName === "VIDEO" && !!target.parentElement?.parentElement?.parentElement?.firstChild?.src) { - targetURL = contextMenuAPIArguments[0].target.parentElement.parentElement.parentElement.firstChild.src; - } else if (target.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.firstChild?.tagName === "VIDEO" && !!target.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.firstChild?.src) { - targetURL = contextMenuAPIArguments[0].target.parentElement.parentElement.parentElement.parentElement.parentElement.firstChild.src; - } - - const isRoleIcon = !!roleIconTarget; - const isCustomEmoji = favoriteableType === "emoji" && !!favoriteableId; - const isUnicodeEmoji = favoriteableType === "emoji" && !favoriteableId; - const isSticker = favoriteableType === "sticker"; - const isAttachment = !!mediaItem; - const emojiRegex = favoriteableId ? new RegExp(`<(a)?:(\\w+):(${favoriteableId})>`) : null; - let emojiName = isUnicodeEmoji ? (favoriteableName?.replaceAll(":", "") || null) : (favoriteableName ?? (emojiRegex?.exec(message.content || "")?.[2] || null)); - let emojiAnimated = isCustomEmoji ? emojiRegex?.exec(message.content || "")?.[1] === "a" : false; - const emojiURL = (isCustomEmoji || isUnicodeEmoji) ? parseURL(targetURL.replace(CDN_BASE, MEDIA_PROXY_BASE)) : null; - const sticker = isSticker ? message.stickerItems?.find(sticker => sticker.id === favoriteableId) : null; - const downloadifyItems: any[] = []; - - function findNestedComponent(components: any[], checkFor: string): any { - for (const innerComponent of components) { - if (innerComponent.emoji?.id === checkFor || innerComponent.emoji?.name === checkFor) { - return innerComponent; - } - - if (innerComponent.components?.length) { - const found = findNestedComponent(innerComponent.components, checkFor); - if (found) { - return found; - } - } - } - return null; - } - - if (isRoleIcon) { - const parsedURL = parseURL(roleIconTarget.src.replace(CDN_BASE, MEDIA_PROXY_BASE)); - const isUnicodeRoleIcon = itemSrc.startsWith(PRIMARY_DOMAIN_BASE); - const isCustomRoleIcon = !isUnicodeRoleIcon; - const roleID = GuildMemberStore.getMember(channel.guild_id, message.author.id)?.iconRoleId; - const role = roleID ? GuildRoleStore.getRole(channel.guild_id, roleID) : null; - const sanitizedRoleName = role ? sanitizeFilename(role.name, allowUnicode) : null; - - downloadifyItems.push( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - parsedURL, - sanitizedRoleName ? `${sanitizedRoleName}-icon` : null, - isCustomRoleIcon ? assetAvailability["role-icon-custom"] : assetAvailability["role-icon-unicode-emoji"], - false - ); - }} - /> - ); - } else if (clanBadgeTarget && !!(message.author as any).primaryGuild) { - const { primaryGuild } = (message.author as any); - const sanitizedGuildTag = sanitizeFilename(primaryGuild.tag, allowUnicode); - - downloadifyItems.push( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - parseURL(clanBadgeTarget.src.replace(CDN_BASE, MEDIA_PROXY_BASE)), - sanitizedGuildTag ? `${sanitizedGuildTag}-clan-badge` : null, - assetAvailability["clan-badge"], - false - ); - }} - /> - ); - } else if (isCustomEmoji) { - const reaction = message.reactions?.find(reaction => reaction.emoji.id === favoriteableId)?.emoji; - const component = message.components?.length ? findNestedComponent(message.components, favoriteableId as string) : null; - emojiAnimated = component?.emoji?.animated || reaction?.animated || emojiAnimated; - emojiName = component?.emoji?.name || reaction?.name || emojiName; - - downloadifyItems.push( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - emojiURL as ParsedURL, - emojiName, - assetAvailability["custom-emoji"], - emojiAnimated - ); - }} - /> - ); - } else if (isUnicodeEmoji) { - const reaction = message.reactions?.find(reaction => reaction.emoji.name === favoriteableName)?.emoji; - const component = message.components?.length ? findNestedComponent(message.components, favoriteableName as string) : null; - emojiName = component?.emoji?.name || reaction?.name || emojiName; - emojiAnimated = false; - - downloadifyItems.push( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - emojiURL as ParsedURL, - emojiName, - assetAvailability["unicode-emoji"], - emojiAnimated - ); - }} - /> - ); - } else if (isSticker) { - let stickerURL; - let stickerSource; - let stickerAnimated; - - if (sticker?.format_type === StickerFormatType.PNG) { - stickerURL = parseURL(`${MEDIA_PROXY_BASE}/stickers/${sticker.id}.png`); - stickerSource = assetAvailability.sticker.PNG; - stickerAnimated = false; - } else if (sticker?.format_type === StickerFormatType.GIF) { - stickerURL = parseURL(`${MEDIA_PROXY_BASE}/stickers/${sticker.id}.gif`); - stickerSource = assetAvailability.sticker.GIF; - stickerAnimated = true; - } else if (sticker?.format_type === StickerFormatType.APNG) { - stickerURL = parseURL(`${MEDIA_PROXY_BASE}/stickers/${sticker.id}.png`); - stickerSource = assetAvailability.sticker.APNG; - stickerAnimated = true; - } else if (sticker?.format_type === StickerFormatType.LOTTIE) { - stickerURL = parseURL(`${CDN_BASE}/stickers/${sticker.id}.json`); - stickerSource = assetAvailability.sticker.LOTTIE; - stickerAnimated = true; - } else { - return; - } - - downloadifyItems.push( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - stickerURL, - sticker.name, - stickerSource, - stickerAnimated - ); - }} - /> - ); - } else if (isAttachment) { - const assetInfo = assetAvailability.attachment[mediaItem.contentType] ?? unknownCDN; - const attachment = message.attachments?.find(attachment => attachment.proxy_url === mediaItem.proxyUrl || attachment.url === mediaItem.url); - - if (!attachment) { - return; - } - - downloadifyItems.push( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - parseURL(assetInfo.source === AssetSource.CDN ? mediaItem.url : mediaItem.proxyUrl), - (attachment as any)?.title || null, - assetInfo, - !!((attachment as any).flags & AttachmentFlags.IS_ANIMATED) - ); - }} - /> - ); - } else if (targetURL) { - let isTenor = false; - - const embedImage = message.embeds?.find(embed => { - const images = (embed as any).images ? (embed as any).images : embed.image ? [embed.image] : []; - return images.some(image => targetURL.startsWith(image.url) || targetURL.startsWith(image.proxyURL)); - }); - - const embedVideo = embedImage ? undefined : message.embeds?.find(embed => { - isTenor = embed.provider?.name === "Tenor"; - const videos = (embed as any).videos ? (embed as any).videos : embed.video ? [embed.video] : []; - return videos.some(video => targetURL.startsWith(video.url) || targetURL.startsWith(video.proxyURL)); - }); - - const embedThumbnail = message.embeds?.find(embed => { - return (embed.thumbnail?.url && targetURL.startsWith(embed.thumbnail.url)) || (embed.thumbnail?.proxyURL && targetURL.startsWith(embed.thumbnail.proxyURL)); - }); - - const embedAuthor = message.embeds?.find(embed => { - return (embed.author?.iconURL && targetURL.startsWith(embed.author.iconURL)) || (embed.author?.iconProxyURL && targetURL.startsWith(embed.author.iconProxyURL)); - }); - - const embedFooter = (message.embeds?.find(embed => { - return ((embed as any).footer?.iconURL && targetURL.startsWith((embed as any).footer.iconURL)) || ((embed as any).footer?.iconProxyURL && targetURL.startsWith((embed as any).footer.iconProxyURL)); - }) as any); - - const targetEmbed = (embedImage || embedVideo || embedThumbnail || embedAuthor || embedFooter); - const targetEmbedItem = (embedImage?.image || embedVideo?.video || embedThumbnail?.thumbnail || embedAuthor?.author || embedFooter?.footer); - const labelEmbedMedia = embedFooter?.footer - ? "Footer Icon" - : embedAuthor?.author - ? "Author Icon" - : embedThumbnail?.thumbnail - ? "Thumbnail Image" - : targetEmbed?.type === "rich" || !!targetEmbed?.provider - ? "Embed Media" - : "Media"; - - downloadifyItems.push( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - let srcIsAnimated = !!(targetEmbedItem as any)?.srcIsAnimated; - let aliasBasename: string | null = null; - let assetInfo: any; - - const contentType = ((targetEmbedItem as any)?.contentType || await DownloadifyNative.queryURL(itemSrc)); - const isMediaProxy = targetURL?.startsWith(MEDIA_PROXY_BASE); - const isPrimaryDomain = targetURL?.startsWith(PRIMARY_DOMAIN_BASE); - const isCDN = targetURL?.startsWith(CDN_BASE); - const isImageExt1 = targetURL?.startsWith(IMAGE_EXT_1_DOMAIN_BASE); - const isImageExt2 = targetURL?.startsWith(IMAGE_EXT_2_DOMAIN_BASE); - const isImageExt = isImageExt1 || isImageExt2; - - if (isTenor && embedVideo) { - targetURL = embedVideo.url ?? targetURL; - } - - const parsedURL = parseURL(targetURL); - - if (isTenor) { - assetInfo = assetAvailability.tenor; - srcIsAnimated = true; - } else if (isImageExt) { - assetInfo = assetAvailability.attachment[contentType] - ?? assetAvailability.external[contentType] - ?? unknownExternalImageProxy; - } else if (isMediaProxy || isCDN) { - const guestimate = guesstimateAsset(parsedURL, contentType); - assetInfo = guestimate.assetInfo ?? (isCDN ? unknownCDN : (isImageExt ? unknownExternalImageProxy : unknownExternal)); - aliasBasename = guestimate.aliasBasename ?? aliasBasename; - srcIsAnimated = guestimate.srcIsAnimated ?? srcIsAnimated; - } else { - assetInfo = isPrimaryDomain ? unknownPrimaryDomain : unknownExternal; - } - - await handleDownload( - parsedURL, - aliasBasename, - assetInfo, - srcIsAnimated - ); - }} - /> - ); - } - - if (!downloadifyItems.length) { - return; - } - - joinOrCreateContextMenuGroup( - children, - downloadifyItems, - "message-content-group", - "downloadify-submenu", - "Download", - [{ - id: { child: "devmode-copy-id" }, - type: "WITH_GROUP", - position: "START" - }] - ); -} - -export function GDMContextMenu(children: Array, props: GDMContextMenuProps): void { - if (!children?.length || !props?.channel?.id) { - return; - } - - const { channel } = props; - const channelIconHash = channel.icon || null; - const channelIconURL = channelIconHash ? parseURL(`${MEDIA_PROXY_BASE}/channel-icons/${channel.id}/${channelIconHash}.png`) : null; - const channelTimestamp = Number(BigInt(channel.id) >> 22n); - const defaultChannelIconURL = channelTimestamp ? parseURL(`${PRIMARY_DOMAIN_BASE}${defaultAssets.DEFAULT_GROUP_DM_AVATARS[channelTimestamp % 8]}`) : null; - - const downloadifyItems = [ - channelIconURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - channelIconURL, - channel.name ? `${channel.name}-group-icon` : null, - assetAvailability["guild-icon"], - false - ); - }} - /> - ) : null, - defaultChannelIconURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => { - await handleDownload( - defaultChannelIconURL, - channel.name ? `${channel.name}-default-group-icon` : null, - assetAvailability["default-group-icon"], - false - ); - }} - /> - ) : null - ].filter(Boolean); - - joinOrCreateContextMenuGroup( - children, - downloadifyItems, - "channel-content-group", - "downloadify-submenu", - "Download", - [{ - id: { child: "devmode-copy-id" }, - type: "WITH_GROUP", - position: "START" - }] - ); -} - -export function GuildContextMenu(children: Array, props: GuildContextMenuProps): void { - if (!children?.length || !props?.guild?.id) { - return; - } - - const { guild } = props; - const guildIconHash = guild.icon || null; - const guildBannerHash = guild.banner || null; - const guildInviteHash = guild.splash !== guildBannerHash ? guild.splash || null : null; - const guildDiscoveryHash = guild.discoverySplash !== guildInviteHash && guild.discoverySplash !== guildBannerHash ? guild.discoverySplash || null : null; - const guildIconHashAnimated = guildIconHash?.startsWith("a_"); - const guildBannerHashAnimated = guildBannerHash?.startsWith("a_"); - const guildIconURL = guildIconHash ? parseURL(`${MEDIA_PROXY_BASE}/icons/${guild.id}/${guildIconHash}.${guildIconHashAnimated ? "gif" : "png"}`) : null; - const guildBannerURL = guildBannerHash ? parseURL(`${MEDIA_PROXY_BASE}/banners/${guild.id}/${guildBannerHash}.${guildBannerHashAnimated ? "gif" : "png"}`) : null; - const guildInviteURL = guildInviteHash ? parseURL(`${MEDIA_PROXY_BASE}/splashes/${guild.id}/${guildInviteHash}.png`) : null; - const guildDiscoveryURL = guildDiscoveryHash ? parseURL(`${MEDIA_PROXY_BASE}/discovery-splashes/${guild.id}/${guildDiscoveryHash}.png`) : null; - - if (!guildIconURL && !guildBannerURL && !guildInviteURL && !guildDiscoveryURL) { - return; - } - - const downloadifyItems = [guildIconURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - guildIconURL, - guild.name ? `${guild.name}-icon` : null, - assetAvailability["guild-icon"], - !!guildIconHash?.startsWith("a_") - )} - /> - ) : null, - guildBannerURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - guildBannerURL, - guild.name ? `${guild.name}-banner` : null, - assetAvailability["guild-banner"], - !!guildBannerHash?.startsWith("a_") - )} - /> - ) : null, - guildInviteURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - guildInviteURL, - guild.name ? `${guild.name}-invite-splash` : null, - assetAvailability["guild-invite-splash"], - !!guildInviteHash?.startsWith("a_") - )} - /> - ) : null, - guildDiscoveryURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - guildDiscoveryURL, - guild.name ? `${guild.name}-discovery-splash` : null, - assetAvailability["guild-discovery-splash"], - !!guildDiscoveryHash?.startsWith("a_") - )} - /> - ) : null].filter(Boolean); - - joinOrCreateContextMenuGroup( - children, - downloadifyItems, - "guild-content-group", - "downloadify-submenu", - "Download", - [{ - id: { child: "devmode-copy-id" }, - type: "WITH_GROUP", - position: "START" - }, - { - id: { child: "privacy" }, - type: "WITH_GROUP", - position: "END" - }] - ); -} - -export function UserContextMenu(children: Array, props: UserContextMenuProps): void { - if (!children?.length || !props.user?.id) { - return; - } - - const user = UserStore.getUser(props.user.id) as CustomizedUser; - const member = (props.guildId ? GuildMemberStore.getMember(props.guildId, props.user.id) : null) as CustomizedMember | null; - const guild = props.guildId ? GuildStore.getGuild(props.guildId) : null; - const userAvatarHash = user.avatar || null; - const memberAvatarHash = member?.avatar || null; - const userBannerHash = UserProfileStore.getUserProfile(user.id)?.banner || null; - const memberBannerHash = UserProfileStore.getGuildMemberProfile(user.id, props.guildId)?.banner || null; - const userAvatarDecorationHash = user.avatarDecorationData?.asset || null; - const memberAvatarDecorationHash = member?.avatarDecoration?.asset || null; - const userNameplatePath = user.collectibles?.nameplate?.asset || null; - const userAvatarURL = userAvatarHash ? parseURL(`${MEDIA_PROXY_BASE}/avatars/${user.id}/${userAvatarHash}.png`) : null; - const memberAvatarURL = memberAvatarHash ? parseURL(`${MEDIA_PROXY_BASE}/guilds/${props.guildId}/users/${user.id}/avatars/${memberAvatarHash}.png`) : null; - const userBannerURL = userBannerHash ? parseURL(`${MEDIA_PROXY_BASE}/banners/${user.id}/${userBannerHash}.png`) : null; - const memberBannerURL = memberBannerHash ? parseURL(`${MEDIA_PROXY_BASE}/guilds/${props.guildId}/users/${user.id}/banners/${memberBannerHash}.png`) : null; - const userAvatarDecorationURL = userAvatarDecorationHash ? parseURL(`${MEDIA_PROXY_BASE}/avatar-decoration-presets/${userAvatarDecorationHash}.png`) : null; - const memberAvatarDecorationURL = memberAvatarDecorationHash ? parseURL(`${MEDIA_PROXY_BASE}/avatar-decoration-presets/${memberAvatarDecorationHash}.png`) : null; - const userNameplateURL = userNameplatePath ? parseURL(`${CDN_BASE}/assets/collectibles/${userNameplatePath}static.png`) : null; - const defaultUserAvatarURL = parseURL(`${PRIMARY_DOMAIN_BASE}${defaultAssets.DEFAULT_AVATARS[user.discriminator === "0" ? (Math.floor(Number(BigInt(user.id) >> 22n)) % 6) : (Number(user.discriminator) % 5)]}`); - - const downloadifyItems = [userAvatarURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - userAvatarURL, - `${user.username}-avatar`, - assetAvailability["user-avatar"], - !!userAvatarHash?.startsWith("a_") - )} - /> - ) : null, - userBannerURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - userBannerURL, - `${user.username}-banner`, - assetAvailability["user-banner"], - !!userBannerHash?.startsWith("a_") - )} - /> - ) : null, - userAvatarDecorationURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - userAvatarDecorationURL, - `${user.username}-avatar-decoration`, - assetAvailability["avatar-decoration"], - true - )} - /> - ) : null, - memberAvatarURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - memberAvatarURL, - guild ? `${guild.name}-${user.username}-avatar` : `${user.username}-avatar`, - assetAvailability["user-avatar"], - !!memberAvatarHash?.startsWith("a_") - )} - /> - ) : null, - memberBannerURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - memberBannerURL, - guild ? `${guild.name}-${user.username}-banner` : `${user.username}-banner`, - assetAvailability["user-banner"], - !!memberBannerHash?.startsWith("a_") - )} - /> - ) : null, - memberAvatarDecorationURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - memberAvatarDecorationURL, - guild ? `${guild.name}-${user.username}-avatar-decoration` : `${user.username}-avatar-decoration`, - assetAvailability["avatar-decoration"], - true - )} - /> - ) : null, - defaultUserAvatarURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - defaultUserAvatarURL, - `${user.username}-default-avatar`, - assetAvailability["default-user-avatar"], - false - )} - /> - ) : null, - userNameplateURL ? ( - ImageIcon({ width: 20, height: 20 })} - action={async () => await handleDownload( - userNameplateURL, - `${user.username}-nameplate`, - assetAvailability.nameplate, - true - )} - /> - ) : null].filter(Boolean); - - joinOrCreateContextMenuGroup( - children, - downloadifyItems, - "user-content-group", - "downloadify-submenu", - "Download", - [{ - id: { child: "devmode-copy-id" }, - type: "WITH_GROUP", - position: "START" - }, - { - id: { child: "user-profile" }, - type: "WITH_GROUP", - position: "END" - }] - ); -} - -export async function handleHoverDownloadButtonClicked(props: HoverDownloadProps): Promise { - let { contentType } = props.item; - const { type, downloadUrl, srcIsAnimated, originalItem } = props.item; - const { displayStatus, statusDuration } = settings.store; - - if (!originalItem) { - // Fallback to default behavior. - DownloadifyLogger.info(`[${getFormattedNow()}] [UNRECOGNIZED HOVER DOWNLOAD ITEM]`, props.item); - displayStatus && showToast("Unknown Media Item. Falling Back to Default Behavior", Toasts.Type.MESSAGE, { duration: statusDuration * 1250 }); - window.open(downloadUrl, "_blank"); - } else { - contentType ??= (await DownloadifyNative.queryURL(downloadUrl) || ""); - const assetInfo = (type === "IMAGE" ? (assetAvailability.attachment[contentType]) : null) ?? unknownCDN; - - await handleDownload( - parseURL(assetInfo.source === AssetSource.CDN ? downloadUrl : originalItem.proxy_url), - originalItem.title || null, - assetInfo, - !!srcIsAnimated - ); - } -} - -export async function handleExpandedModalDownloadButtonClicked(props: ExpandedModalDownloadProps, fallback: () => Promise): Promise { - let { contentType, srcIsAnimated } = props.item; - const { url, original, proxyUrl, sourceMetadata } = props.item; - const { displayStatus, statusDuration } = settings.store; - - if (!url && !original) { - // Fallback to default behavior. - DownloadifyLogger.info(`[${getFormattedNow()}] [UNRECOGNIZED EXPANDED MODAL DOWNLOAD ITEM]`, props.item); - displayStatus && showToast("Unknown Media Item. Falling Back to Default Behavior", Toasts.Type.MESSAGE, { duration: statusDuration * 1250 }); - await fallback(); - } else { - contentType ??= (await DownloadifyNative.queryURL(url) || ""); - let aliasBasename = sourceMetadata?.identifier.title ?? null; - const isAttachment = sourceMetadata?.identifier.type === "attachment"; - const urlIsMediaProxy = url?.startsWith(MEDIA_PROXY_BASE); - const originalIsMediaProxy = original?.startsWith(MEDIA_PROXY_BASE); - const isMediaProxy = urlIsMediaProxy || originalIsMediaProxy; - const isPrimaryDomain = original?.startsWith(PRIMARY_DOMAIN_BASE); - const isCDN = original?.startsWith(CDN_BASE); - const isImageExt1 = url?.startsWith(IMAGE_EXT_1_DOMAIN_BASE); - const isImageExt2 = url?.startsWith(IMAGE_EXT_2_DOMAIN_BASE); - const isImageExt = isImageExt1 || isImageExt2; - let parsedURL: ParsedURL; - let assetInfo: any; - - if (isAttachment) { - parsedURL = parseURL(proxyUrl || url); - assetInfo = assetAvailability.attachment[contentType] ?? unknownAttachmentMediaProxy; - } else if (isImageExt) { - parsedURL = parseURL(url); - assetInfo = assetAvailability.attachment[contentType] ?? assetAvailability.external[contentType] ?? unknownExternalImageProxy; - } else if (isMediaProxy || isCDN) { - parsedURL = parseURL(urlIsMediaProxy ? url : original); - const guestimate = guesstimateAsset(parsedURL, contentType); - assetInfo = guestimate.assetInfo ?? (isCDN ? unknownCDN : unknownExternal); - aliasBasename = guestimate.aliasBasename ?? aliasBasename; - srcIsAnimated = guestimate.srcIsAnimated ?? srcIsAnimated; - } else { - parsedURL = parseURL(original ?? url); - assetInfo = isPrimaryDomain ? unknownPrimaryDomain : unknownExternal; - } - - await handleDownload( - parsedURL, - aliasBasename, - assetInfo, - !!srcIsAnimated, - ); - } -} - -function guesstimateAsset(parsedURL: ParsedURL, contentType: string): { - assetInfo?: AssetInfo; - aliasBasename?: string; - srcIsAnimated?: boolean; -} { - // Check if the URL is a known asset type. The only defined member - // of assetAvailability that we can't completely check for here are stickers - // due to their format information not being present in the URL. We can make - // accurate guesses based on certain extensions, but not for every format. - let assetInfo: AssetInfo | undefined; - let aliasBasename: string | undefined; - let srcIsAnimated: boolean | undefined; - const detected = parsedURL.url.match(ASSET_TYPE_EXTRACTOR); - const detectedPrimary = detected?.[1] ?? ""; - const detectedSecondary = detected?.[2] ?? ""; - const detectedTertiary = detected?.[3] ?? ""; - - if (detectedPrimary === "attachments") { - assetInfo = assetAvailability.attachment[contentType]; - } else if (detectedPrimary === "emojis") { - assetInfo = assetAvailability["custom-emoji"]; - } else if (detectedPrimary === "clan-badges") { - assetInfo = assetAvailability["clan-badge"]; - } else if (detectedPrimary === "role-icons") { - assetInfo = assetAvailability["role-icon-custom"]; - } else if (detectedPrimary === "discovery-splashes") { - assetInfo = assetAvailability["guild-discovery-splash"]; - } else if (detectedPrimary === "splashes") { - assetInfo = assetAvailability["guild-invite-splash"]; - } else if (detectedPrimary === "banners") { - assetInfo = assetAvailability["guild-banner"]; - } else if (detectedPrimary === "icons") { - assetInfo = assetAvailability["guild-icon"]; - } else if (detectedPrimary === "avatar-decoration-presets") { - assetInfo = assetAvailability["avatar-decoration"]; - srcIsAnimated = true; // Avatar decorations always have animated variants. - } else if (detectedPrimary === "avatars" || (detectedPrimary === "guilds" && detectedSecondary === "avatars")) { - assetInfo = assetAvailability["user-avatar"]; - } else if (detectedPrimary === "banners" || (detectedPrimary === "guilds" && detectedSecondary === "banners")) { - assetInfo = assetAvailability["user-banner"]; - } else if (detectedPrimary === "assets/collectibles/nameplates") { - assetInfo = assetAvailability.nameplate; - aliasBasename = detectedTertiary ?? "nameplate"; - srcIsAnimated = true; // Nameplates always have animated variants. - } else if (detectedPrimary === "stickers") { - if (parsedURL.extension === "gif") { - assetInfo = assetAvailability.sticker.GIF; - } else if (parsedURL.extension === "webp") { - if (srcIsAnimated) { - assetInfo = assetAvailability.sticker.GIF; - } else { - // There is no way to know if a static WEBP sticker - // link is sourced from an APNG, PNG, or GIF sticker. - } - } else if (parsedURL.extension === "png") { - if (srcIsAnimated) { - assetInfo = assetAvailability.sticker.PNG; - } else { - // There is no way to know if a static PNG sticker - // link is sourced from an APNG, PNG, or GIF sticker. - } - } else if (parsedURL.extension === "jpg") { - // There is no way to know if a JPG sticker link - // is sourced from an APNG, PNG, or GIF sticker. - } - } - - return { - assetInfo, - aliasBasename, - srcIsAnimated - }; -} - -export function VoiceMessageDownloadButton(props: VoiceMessageDownloadButtonProps): JSX.Element { - async function voiceMessageDownloadClicked(event: React.MouseEvent) { - event.preventDefault(); - const { item, message } = props; - const { proxy_url } = item.originalItem; - const baseName = `voice-message-${message.id}`; - const parsedURL = parseURL(proxy_url); - - await handleDownload( - parsedURL, - baseName, - { source: AssetSource.ATTACHMENT_MEDIA_PROXY, static: [], animated: [] }, - false, - ); - } - - const { voiceMessages } = settings.use(["voiceMessages"]); - - return ( - <> - {voiceMessages &&
- -
} - - ); -} - -/** - * Common download handler for all other handlers. - */ -async function handleDownload( - url: ParsedURL, - alias: string | null, - asset: AssetInfo, - animated: boolean, -) { - const extensions = animated || !!asset.forceAnimated ? asset.animated : asset.static; - extensions.length < 1 && url.extension && extensions.push(url.extension); - const { defaultDirectory, overwriteFiles, displayStatus, allowUnicode, statusDuration } = settings.store; - const baseName = alias - ? sanitizeFilename(alias, allowUnicode, "discord-download") - : url.baseName - ? sanitizeFilename(url.baseName, allowUnicode, "discord-download") - : "discord-download"; - - let chosenExtension = extensions[0] ?? ""; - let resolvedExtension = (chosenExtension).replace("apng", "png").replace("awebp", "webp"); - let resolvedPath: string | null = null; - let resolvedURL = url.url; - - if (defaultDirectory) { - const resolvedDirectory = defaultDirectory.trim().replace(/^["']|["']$/g, "").replace(/[/\\]+$/, "").replace(/\\/g, "/"); - resolvedPath = `${resolvedDirectory}/${baseName}${resolvedExtension ? `.${resolvedExtension}` : ""}`; - displayStatus && showToast("Download Started", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); - - if (!overwriteFiles) { - for (let num = 1; await DownloadifyNative.fileExists(resolvedPath); num++) { - resolvedPath = `${resolvedDirectory}/${baseName}-${num}${resolvedExtension ? `.${resolvedExtension}` : ""}`; - } - } - } else { - try { - resolvedPath = await DownloadifyNative.getFilePath(baseName, extensions); - - if (!resolvedPath) { - DownloadifyLogger.info(`[${getFormattedNow()}] [SAVE DIALOGUE CLOSED / DOWNLOAD CANCELED]`); - displayStatus && showToast("Download Canceled", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); - return; - } - - const chosenFile = parseFile(resolvedPath); - chosenExtension = chosenFile.extension || ""; - resolvedExtension = (chosenExtension || "").replace("apng", "png").replace("awebp", "webp"); - - if (url.extension) { - const extReplacer = new RegExp(`\\.${chosenExtension}(?!.*\\.${chosenExtension})`, "i"); - resolvedPath = resolvedPath.replace(extReplacer, `.${resolvedExtension}`); - } - } catch (error) { - DownloadifyLogger.error(`[${getFormattedNow()}] [ERROR OPENING SAVE DIALOGUE]:`, error); - displayStatus && showToast("Error Opening Save Dialogue", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); - return; - } - } - - try { - let success = false; - - if ([AssetSource.ASSET_MEDIA_PROXY, AssetSource.ATTACHMENT_MEDIA_PROXY, AssetSource.EXTERNAL_IMAGE_PROXY].includes(asset.source)) { - let baseURL; - - if (asset.source === AssetSource.ASSET_MEDIA_PROXY) { - baseURL = url.host + url.path + url.baseName + (resolvedExtension ? `.${resolvedExtension}` : ""); - } else { - // Attachments on the media proxy use the `format` query parameter (seen below) to retrieve alternate - // formats instead of changing the file extension directly like assets on the media proxy do. - baseURL = url.host + url.path + url.baseName + - (url.extension ? `.${url.extension}` : "") + - (url.twitterExtra ? encodeURIComponent(url.twitterExtra) : ""); - } - - const authParams = url.params.expiry; - const newURL = new URL(baseURL); - - Object.entries(authParams).forEach(([key, value]) => { - newURL.searchParams.append(key, value); - }); - - if ([AssetSource.ATTACHMENT_MEDIA_PROXY, AssetSource.EXTERNAL_IMAGE_PROXY].includes(asset.source)) { - if (url.extension !== resolvedExtension) { - newURL.searchParams.append("format", resolvedExtension.replace("jpg", "jpeg")); - } - } else { - // Attachments on the media proxy do not support - // resizing but assets on the media proxy do. - newURL.searchParams.append("size", "4096"); - } - - if (chosenExtension === "awebp" && animated) { - newURL.searchParams.append("animated", "true"); - } else if (chosenExtension === "png" && animated) { - newURL.searchParams.append("passthrough", "false"); - } - - if (resolvedExtension === "webp") { - newURL.searchParams.append("lossless", "true"); - } - - resolvedURL = newURL.href; - } else if (asset.source === AssetSource.CDN) { - if (asset.type === "nameplate") { - resolvedURL = url.host + url.path + ( - chosenExtension === "apng" - ? "img.png" - : chosenExtension === "webm" - ? "asset.webm" - : "static.png" - ); - } else { - // Currently only the LOTTIE sticker. - resolvedURL = url.url; - } - } else if (asset.source === AssetSource.TENOR) { - const tenorID = url.path.replaceAll("/", "").slice(0, -2); - - if (chosenExtension === "gif") { - resolvedURL = `${TENOR_BASE}/${tenorID}${TENOR_GIF_ID}/tenor.gif`; - } else if (chosenExtension === "mp4") { - resolvedURL = `${TENOR_BASE}/${tenorID}${TENOR_MP4_ID}/tenor.mp4`; - } else { - DownloadifyLogger.warn(`[${getFormattedNow()}] [UNSUPPORTED TENOR EXTENSION]\n${chosenExtension}\n${resolvedPath}\n${resolvedURL}`); - displayStatus && showToast("Unsupported Tenor Extension", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); - return; - } - } else if ([AssetSource.EXTERNAL, AssetSource.PRIMARY_DOMAIN].includes(asset.source)) { - resolvedURL = url.url; - } - - DownloadifyLogger.info(`[${getFormattedNow()}] [STARTING DOWNLOAD]\n${resolvedPath}\n${resolvedURL}`); - success = await DownloadifyNative.downloadURL(resolvedURL, resolvedPath); - - if (success) { - DownloadifyLogger.info(`[${getFormattedNow()}] [DOWNLOAD SUCCESS]\n${resolvedPath}\n${resolvedURL}`); - displayStatus && showToast("File Downloaded", Toasts.Type.SUCCESS, { duration: statusDuration * 1000 }); - } else { - DownloadifyLogger.error(`[${getFormattedNow()}] [DOWNLOAD FAILED]\n${resolvedPath}\n${resolvedURL}`); - displayStatus && showToast("File Download Failed", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); - } - } catch (error) { - DownloadifyLogger.error(`[${getFormattedNow()}] [DOWNLOAD ERRORED]\n${resolvedPath}\n${resolvedURL}`, error); - displayStatus && showToast("Error Downloading File", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); - } -} diff --git a/src/equicordplugins/downloadify.desktop/utils/misc.tsx b/src/equicordplugins/downloadify.desktop/utils/misc.tsx deleted file mode 100644 index ab1119fb4..000000000 --- a/src/equicordplugins/downloadify.desktop/utils/misc.tsx +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { findGroupChildrenByChildId } from "@api/ContextMenu"; -import { Menu } from "@webpack/common"; - -import { InsertGroup, ParsedFile, ParsedURL } from "./definitions"; - -/** - * Retrieve information about a file path by parsing its parts. - */ -export function parseFile(filePath: string): ParsedFile { - const parts = filePath.split(/[\\/]/); - let fileNameWithExtra = parts.pop() as string; - - while (fileNameWithExtra.includes("%25")) { - fileNameWithExtra = decodeURIComponent(fileNameWithExtra); - } - - fileNameWithExtra = decodeURIComponent(fileNameWithExtra); - - const posColon = fileNameWithExtra.indexOf(":"); - const fileNameWithoutExtra = posColon === -1 ? fileNameWithExtra : fileNameWithExtra.slice(0, posColon); - const twitterExtra = posColon === -1 ? null : fileNameWithExtra.slice(posColon); - - const path = parts.join("/") + "/"; - const posDot = fileNameWithoutExtra.lastIndexOf("."); - - if (fileNameWithoutExtra === "" || posDot < 1) { - return { - path, - baseName: fileNameWithoutExtra, - extension: null, - twitterExtra: null - }; - } - - return { - path, - baseName: fileNameWithoutExtra.slice(0, posDot), - extension: fileNameWithoutExtra.slice(posDot + 1).toLowerCase(), - twitterExtra: twitterExtra ?? null - }; -} - -/** - * Retrieve information about a URL by parsing its parts. - */ -export function parseURL(url: string): ParsedURL { - const parsed = new URL(url); - const { path, baseName, extension, twitterExtra } = parseFile(parsed.pathname); - const params = Array.from(parsed.searchParams.entries()).reduce( - (acc, [key, value]) => { - if (["ex", "is", "hm"].includes(key)) { - acc.expiry[key] = value; - } else { - acc.other[key] = value; - } - - return acc; - }, { expiry: {}, other: {} } as { expiry: Record, other: Record; } - ); - - return { - url: url, - host: parsed.origin.toLowerCase(), - path, - baseName, - extension, - params, - twitterExtra - }; -} - -/** - * Sanitize a file name by replacing invalid characters with underscores. - */ -export function sanitizeFilename(filename: string, allowUnicode: boolean, fallback?: string): string { - let sanitized = filename.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_") // windows-reserved - .replace(/^\./, "_") // dot files - .replace(/(\.\.)+/g, "_") // relative paths - .replace(/[ \t\n\r]+/g, "_"); // spaces - - if (!allowUnicode) { - sanitized = sanitized.replace(/[^\x00-\x7F]/g, "_"); // non-ASCII characters - } else { - sanitized = sanitized.normalize("NFD"); // normalize unicode - } - - sanitized = sanitized.replace(/^_+|_+$/g, ""); // leading and trailing underscores - return sanitized || fallback || ""; -} - -/** - * Get the current date and time formatted as a string. - */ -export function getFormattedNow(): string { - return new Date().toLocaleString(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: true - }).replace(",", ""); -} - -/** - * Join or create a group in the guild context menu. - */ -export function joinOrCreateContextMenuGroup( - children: Array, - items: Array, - groupId: string, - submenuId?: string, - submenuLabel?: string, - insertOrder?: InsertGroup[] -) { - function joinOrCreateSubmenu() { - const existingItemShouldJoinSubmenu = existingGroup.props.children.find(child => child?.props?.submenuId === submenuId); - let existingSubmenu = existingGroup.props.children.find(child => child?.props?.id === submenuId); - let numitems = items.length; - - if (existingItemShouldJoinSubmenu) { - existingSubmenu = null; - items.unshift(existingItemShouldJoinSubmenu); - numitems++; - const indexOfExistingItemShouldJoinSubmenu = existingGroup.props.children.findIndex(child => child === existingItemShouldJoinSubmenu); - existingGroup.props.children.splice(indexOfExistingItemShouldJoinSubmenu, 1); - } - - if (existingSubmenu) { - if (!Array.isArray(existingSubmenu.props.children)) { - existingSubmenu.props.children = [existingSubmenu.props.children]; - } - - items.forEach(member => member.props.label = member.props.submenuItemLabel ?? member.props.label); - existingSubmenu.props.children.push(...items); - } else if (numitems === 1) { - if (!chosenInsertOption?.position || chosenInsertOption.position === "END") { - existingGroup.props.children.push(items[0]); - } else { - existingGroup.props.children.unshift(items[0]); - } - } else { - existingSubmenu = ( - - {[...items]} - - ); - - items.forEach(member => member.props.label = member.props.submenuItemLabel ?? member.props.label); - - if (!chosenInsertOption?.position || chosenInsertOption.position === "END") { - existingGroup.props.children.push(existingSubmenu); - } else { - existingGroup.props.children.unshift(existingSubmenu); - } - } - } - - items.forEach(member => { - member.props.groupId = groupId; - member.props.submenuId = submenuId; - member.props.submenuLabel = submenuLabel; - }); - - const insertWithOptions = insertOrder?.filter(item => item.type === "WITH_GROUP"); - let existingGroup = children.find(child => child?.props?.id === groupId); - let chosenInsertOption; - - if (!existingGroup && insertWithOptions) { - let targetItem; - - for (const item of insertWithOptions) { - if (item.id.group) { - targetItem = children.find(child => child?.props?.id === item.id.group); - } - - if (!targetItem && item.id.child) { - targetItem = findGroupChildrenByChildId(item.id.child, children, true); - } - - if (targetItem) { - existingGroup = children.find(child => child?.props?.children === targetItem); - chosenInsertOption = item; - break; - } - } - } - - if (existingGroup) { - if (!Array.isArray(existingGroup.props.children)) { - existingGroup.props.children = [existingGroup.props.children]; - } - - if (!submenuId) { - if (!chosenInsertOption?.position || chosenInsertOption.position === "END") { - existingGroup.props.children.push(...items); - } else { - existingGroup.props.children.unshift(...items); - } - - return; - } else { - joinOrCreateSubmenu(); - } - } else { - if (!submenuId) { - existingGroup = ( - - {[...items]} - - ); - } else { - existingGroup = ({[]}); - joinOrCreateSubmenu(); - } - - const insertAfterBeforeOptions = insertOrder?.filter(item => item.type !== "WITH_GROUP") || []; - let targetIndex = children.length; - let targetItem; - - for (const item of insertAfterBeforeOptions) { - if (item.id.group) { - targetItem = children.find(child => child?.props?.id === item.id.group); - } - - if (!targetItem && item.id.child) { - targetItem = findGroupChildrenByChildId(item.id.child, children, true); - } - - if (targetItem) { - targetIndex = children.findIndex(child => child?.props?.children === targetItem); - - if (item.type === "AFTER_GROUP") { - targetIndex++; - } - - break; - } - } - - children.splice(targetIndex, 0, existingGroup); - } -} diff --git a/src/equicordplugins/equicordHelper/utils.tsx b/src/equicordplugins/equicordHelper/utils.tsx index 5d67bd929..57d1f072a 100644 --- a/src/equicordplugins/equicordHelper/utils.tsx +++ b/src/equicordplugins/equicordHelper/utils.tsx @@ -100,3 +100,31 @@ export async function toggleEnabled(name: string) { settings.enabled = !wasEnabled; return await beforeReturn(settings, wasEnabled); } + +export function getWindowsName(release: string) { + const build = parseInt(release.split(".")[2]); + if (build >= 22000) return "Windows 11"; + if (build >= 10240) return "Windows 10"; + if (build >= 9200) return "Windows 8.1"; + if (build >= 7600) return "Windows 7"; + return `Windows (${release})`; +} + +export function getMacOSName(release: string) { + const major = parseInt(release.split(".")[0]); + if (major === 24) return "MacOS 15 (Sequoia)"; + if (major === 23) return "MacOS 14 (Sonoma)"; + if (major === 22) return "MacOS 13 (Ventura)"; + if (major === 21) return "MacOS 12 (Monterey)"; + if (major === 20) return "MacOS 11 (Big Sur)"; + if (major === 19) return "MacOS 10.15 (Catalina)"; + return `MacOS (${release})`; +} + +export function platformName() { + if (typeof DiscordNative === "undefined") return navigator.platform; + if (DiscordNative.process.platform === "win32") return `${getWindowsName(DiscordNative.os.release)}`; + if (DiscordNative.process.platform === "darwin") return `${getMacOSName(DiscordNative.os.release)} (${DiscordNative.process.arch === "arm64" ? "Apple Silicon" : "Intel Silicon"})`; + if (DiscordNative.process.platform === "linux") return `Linux (${DiscordNative.os.release})`; + return DiscordNative.process.platform; +} diff --git a/src/equicordplugins/forwardAnywhere/index.ts b/src/equicordplugins/forwardAnywhere/index.ts index 0f38b467b..528548223 100644 --- a/src/equicordplugins/forwardAnywhere/index.ts +++ b/src/equicordplugins/forwardAnywhere/index.ts @@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { sendMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { Message } from "@vencord/discord-types"; +import { Channel, Message } from "@vencord/discord-types"; // Taken From Signature :) const settings = definePluginSettings({ @@ -43,11 +43,21 @@ export default definePlugin({ } }, ], - sendForward(channels: any, message: Message) { - for (const c of channels) { - sendMessage(c.id, { - content: `${message.content}\n${settings.store.forwardPreface} Forwarded from <#${message.channel_id}>` - }); - } + + sendForward(channels: Channel[], message: Message) { + const chunkSize = 5; + channels.forEach(c => { + if (message.attachments.length) { + for (let i = 0; i < message.attachments.length; i += chunkSize) { + const group = message.attachments.slice(i, i + chunkSize); + const text = `${message.content}\nAttachments:\n${group.map(a => a.url).join("\n")}\n${settings.store.forwardPreface} Forwarded from <#${message.channel_id}>`; + sendMessage(c.id, { content: text }); + } + } else { + sendMessage(c.id, { + content: `${message.content}\n${settings.store.forwardPreface} Forwarded from <#${message.channel_id}>` + }); + } + }); } }); diff --git a/src/equicordplugins/friendCodes/FriendCodesPanel.tsx b/src/equicordplugins/friendCodes/FriendCodesPanel.tsx index 482203969..40c875d7b 100644 --- a/src/equicordplugins/friendCodes/FriendCodesPanel.tsx +++ b/src/equicordplugins/friendCodes/FriendCodesPanel.tsx @@ -99,7 +99,7 @@ export default function FriendCodesPanel() { ); diff --git a/src/equicordplugins/githubRepos/components/ReposModal.tsx b/src/equicordplugins/githubRepos/components/ReposModal.tsx index 67d13dbb1..eeac8e39d 100644 --- a/src/equicordplugins/githubRepos/components/ReposModal.tsx +++ b/src/equicordplugins/githubRepos/components/ReposModal.tsx @@ -91,7 +91,7 @@ export function ReposModal({ repos, username, rootProps }: ReposModalProps) { ) + } + + + : Loading song link... + } + ; +} diff --git a/src/equicordplugins/songLink.desktop/index.tsx b/src/equicordplugins/songLink.desktop/index.tsx new file mode 100644 index 000000000..2961f1bd7 --- /dev/null +++ b/src/equicordplugins/songLink.desktop/index.tsx @@ -0,0 +1,87 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 nin0 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType, PluginNative } from "@utils/types"; + +import Providers from "./Providers"; +import { Settings } from "./Settings"; +import SongLinker from "./SongLinker"; + +export const settings = definePluginSettings({ + servicesSettings: { + type: OptionType.CUSTOM, + description: "settings for services", + default: Object.fromEntries(Object.entries(Providers).map(([name, data]) => [name, { + enabled: true, + // @ts-ignore + openInNative: data.native || false + }])) + }, + userCountry: { + type: OptionType.STRING, + description: "Country used for lookup (Two letter country code)", + default: "US" + }, + servicesComponent: { + type: OptionType.COMPONENT, + component: () => + } +}); + +export type SongLinkResult = { + info?: { + title: string; + artist: string; + }; + links: { + [platform: string]: { + url: string; + nativeUri?: string; + }; + }; +}; + +export const Native = VencordNative.pluginHelpers.SongLink as PluginNative; + +export default definePlugin({ + name: "SongLink", + description: "Adds streaming service buttons below song links", + authors: [Devs.nin0dev], + settings, + Providers, + cache: ({} as Record), + addToCache(link, data: SongLinkResult) { + this.cache[link] = data; + }, + renderMessageAccessory(props: Record) { + const { content }: { + content: string; + } = props.message; + if (!content) return; + + const regexes = [ + /https:\/\/(?:open|play)\.spotify\.com\/track\/[a-zA-Z0-9]+/, // spotify + /https:\/\/(music|itunes)\.apple\.com\/[a-z]{2}\/album\/\S+/, // apple music/itunes + /https:\/\/music\.youtube\.com\/watch\?v=[0-9A-Za-z_-]+/, // yt music + /https:\/\/tidal\.com\/track\/[0-9]+\/u/ // tidal + ]; + const musicLinks = content.match(new RegExp(regexes.map(r => r.source).join("|"), "g")); + if (!musicLinks?.length) return; + + return
+ { + musicLinks.map(item => ) + } +
; + } +}); diff --git a/src/equicordplugins/songLink.desktop/native.ts b/src/equicordplugins/songLink.desktop/native.ts new file mode 100644 index 000000000..f4a501f08 --- /dev/null +++ b/src/equicordplugins/songLink.desktop/native.ts @@ -0,0 +1,40 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 nin0 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { RendererSettings } from "@main/settings"; + +type SongLinkResult = { + info?: { + title: string; + artist: string; + }; + links: { + [platform: string]: { + url: string; + nativeUri?: string; + }; + }; +}; + +export async function getTrackData(_, trackURL: string): Promise { + const url = new URL("https://api.song.link/v1-alpha.1/links"); + url.searchParams.set("url", trackURL); + url.searchParams.set("userCountry", RendererSettings.store.plugins?.SongLink.userCountry || "US"); + const raw = await fetch(url.toString()).then(u => u.json()); + const [, entry]: any = Object.entries(raw.entitiesByUniqueId) + .find(([key]) => !key.includes("YOUTUBE")) || []; + const possibleTrackInfo = entry + ? { title: (entry.title as string), artist: (entry.artistName as string) } + : null; + return { + // @ts-ignore + info: possibleTrackInfo, + links: Object.fromEntries(Object.entries(raw.linksByPlatform).map(([name, data]) => [name, { + url: data.url, + nativeUri: data.nativeAppUriDesktop + }])) + }; +} diff --git a/src/equicordplugins/soundBoardLogger/components/Icons.tsx b/src/equicordplugins/soundBoardLogger/components/Icons.tsx index b1b2ae713..b4314d169 100644 --- a/src/equicordplugins/soundBoardLogger/components/Icons.tsx +++ b/src/equicordplugins/soundBoardLogger/components/Icons.tsx @@ -42,7 +42,7 @@ export function IconWithTooltip({ text, icon, onClick }) { aria-haspopup="dialog" aria-label={text} size="" - look={Button.Looks.LINK} + look={Button.Looks.FILLED} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} className={ButtonWrapperClasses.button} diff --git a/src/equicordplugins/themeLibrary/components/LikesComponent.tsx b/src/equicordplugins/themeLibrary/components/LikesComponent.tsx index 237fc9f68..dc6623c59 100644 --- a/src/equicordplugins/themeLibrary/components/LikesComponent.tsx +++ b/src/equicordplugins/themeLibrary/components/LikesComponent.tsx @@ -82,7 +82,7 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t