diff --git a/README.md b/README.md index cb564bc1..5eb50550 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ Equicord is a fork of [Vencord](https://github.com/Vendicated/Vencord), with over 300+ plugins. -You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, changes, chat or even support.


+You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, changes, chat or even support. ### Extra included plugins
-183 additional plugins +186 additional plugins ### All Platforms @@ -20,6 +20,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - AlwaysExpandProfile by thororen - AmITyping by MrDiamond - Anammox by Kyuuhachi +- AudiobookShelfRPC by vMohammad - AtSomeone by Joona - BannersEverywhere by ImLvna & AutumnVN - BetterActivities by D3SOX, Arjix, AutumnVN @@ -95,6 +96,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - InRole by nin0dev - InstantScreenshare by HAHALOSAH & thororen - IRememberYou by zoodogood +- JellyfinRichPresence by vMohammad - Jumpscare by Surgedevs - JumpToStart by Samwich - KeyboardSounds by HypedDomi @@ -160,6 +162,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - StatusPresets by iamme - SteamStatusSync by niko - StickerBlocker by Samwich +- StreamingCodecDisabler by davidkra230 - TalkInReverse by Tolgchu - TeX by Kyuuhachi - TextToSpeech by Samwich diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index da5157f8..283150d3 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -20,15 +20,12 @@ /// import monacoHtmlLocal from "file://monacoWin.html?minify"; -import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify"; import * as DataStore from "../src/api/DataStore"; -import { debounce } from "../src/utils"; +import { debounce, localStorage } from "../src/utils"; import { EXTENSION_BASE_URL } from "../src/utils/web-metadata"; import { getTheme, Theme } from "../src/utils/discord"; import { Settings } from "../src/Vencord"; - -// Discord deletes this so need to store in variable -const { localStorage } = window; +import { getStylusWebStoreUrl } from "@utils/web"; // listeners for ipc.on const cssListeners = new Set<(css: string) => void>(); @@ -76,6 +73,14 @@ window.VencordNative = { addThemeChangeListener: NOOP, openFile: NOOP_ASYNC, async openEditor() { + if (IS_USERSCRIPT) { + const shouldOpenWebStore = confirm("QuickCSS is not supported on the Userscript. You can instead use the Stylus extension.\n\nDo you want to open the Stylus web store page?"); + if (shouldOpenWebStore) { + window.open(getStylusWebStoreUrl(), "_blank"); + } + return; + } + const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`; const win = open("about:blank", "VencordQuickCss", features); if (!win) { @@ -91,7 +96,7 @@ window.VencordNative = { ? "vs-light" : "vs-dark"; - win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn); + win.document.write(monacoHtmlLocal); }, }, diff --git a/package.json b/package.json index a205c0ba..c7020e76 100644 --- a/package.json +++ b/package.json @@ -1,100 +1,102 @@ { - "name": "equicord", - "private": "true", - "version": "1.12.2", - "description": "The other cutest Discord client mod", - "homepage": "https://github.com/Equicord/Equicord#readme", - "bugs": { - "url": "https://github.com/Equicord/Equicord/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Equicord/Equicord.git" - }, - "license": "GPL-3.0-or-later", - "author": "Equicord", - "scripts": { - "build": "bun run scripts/build/build.mjs", - "buildStandalone": "bun run build --standalone", - "buildWeb": "bun run scripts/build/buildWeb.mjs", - "buildWebStandalone": "bun run buildWeb --standalone", - "buildReporter": "bun run buildWebStandalone --reporter --skip-extension", - "buildReporterDesktop": "bun run build --reporter", - "watch": "bun run build --watch", - "dev": "bun run watch", - "watchWeb": "bun run buildWeb --watch", - "generatePluginJson": "bun run scripts/generatePluginList.ts", - "generateEquicordPluginJson": "bun run scripts/generateEquicordPluginList.ts", - "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false", - "inject": "bun run scripts/runInstaller.mjs --install", - "uninject": "bun run scripts/runInstaller.mjs --uninstall", - "lint": "eslint", - "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", - "lint:fix": "bun run lint --fix", - "test": "bun run buildStandalone && bun run testTsc && bun run lint:fix && bun run lint-styles && bun run generatePluginJson", - "testWeb": "bun run lint && bun run buildWeb && bun run testTsc", - "testTsc": "tsc --noEmit" - }, - "dependencies": { - "@ffmpeg/ffmpeg": "^0.12.10", - "@ffmpeg/util": "^0.12.1", - "@intrnl/xxhash64": "^0.1.2", - "@sapphi-red/web-noise-suppressor": "0.3.5", - "@types/less": "^3.0.6", - "@types/stylus": "^0.48.42", - "@types/tinycolor2": "^1.4.6", - "@vap/core": "0.0.12", - "@vap/shiki": "0.10.5", - "fflate": "^0.8.2", - "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", - "jsqr": "1.4.0", - "idb": "8.0.0", - "monaco-editor": "^0.52.2", - "nanoid": "^5.1.5", - "socket.io": "^4.8.1", - "usercss-meta": "^0.12.0", - "openai": "^4.30.0", - "virtual-merge": "^1.0.1" - }, - "devDependencies": { - "@electron/asar": "^3.2.10", - "@stylistic/eslint-plugin": "^4.2.0", - "@types/chrome": "^0.0.312", - "@types/diff": "^7.0.2", - "@types/lodash": "^4.17.14", - "@types/node": "^22.13.13", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@types/yazl": "^2.4.5", - "diff": "^7.0.0", - "discord-types": "^1.3.26", - "esbuild": "^0.25.1", - "eslint": "^8.57.0", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-plugin-react": "^7.37.3", - "eslint-plugin-simple-header": "^1.2.1", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-unused-imports": "^4.1.4", - "highlight.js": "11.11.1", - "html-minifier-terser": "^7.2.0", - "moment": "^2.22.2", - "puppeteer-core": "^24.4.0", - "standalone-electron-types": "^34.2.0", - "stylelint": "^16.17.0", - "stylelint-config-standard": "^37.0.0", - "ts-patch": "^3.3.0", - "ts-pattern": "^5.6.0", - "type-fest": "^4.41.0", - "typescript": "^5.8.2", - "typescript-eslint": "^8.28.0", - "typescript-transform-paths": "^3.5.5", - "zip-local": "^0.3.5", - "zustand": "^3.7.2" - }, - "packageManager": "bun@1.1.0", - "trustedDependencies": ["esbuild"], - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - } -} + "name": "equicord", + "private": "true", + "version": "1.12.4", + "description": "The other cutest Discord client mod", + "homepage": "https://github.com/Equicord/Equicord#readme", + "bugs": { + "url": "https://github.com/Equicord/Equicord/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Equicord/Equicord.git" + }, + "license": "GPL-3.0-or-later", + "author": "Equicord", + "scripts": { + "build": "bun run scripts/build/build.mjs", + "buildStandalone": "bun run build --standalone", + "buildWeb": "bun run scripts/build/buildWeb.mjs", + "buildWebStandalone": "bun run buildWeb --standalone", + "buildReporter": "bun run buildWebStandalone --reporter --skip-extension", + "buildReporterDesktop": "bun run build --reporter", + "watch": "bun run build --watch", + "dev": "bun run watch", + "watchWeb": "bun run buildWeb --watch", + "generatePluginJson": "bun run scripts/generatePluginList.ts", + "generateEquicordPluginJson": "bun run scripts/generateEquicordPluginList.ts", + "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false", + "inject": "bun run scripts/runInstaller.mjs --install", + "uninject": "bun run scripts/runInstaller.mjs --uninstall", + "lint": "eslint", + "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", + "lint:fix": "bun run lint --fix", + "test": "bun run buildStandalone && bun run testTsc && bun run lint:fix && bun run lint-styles && bun run generatePluginJson", + "testWeb": "bun run lint && bun run buildWeb && bun run testTsc", + "testTsc": "tsc --noEmit" + }, + "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/util": "^0.12.1", + "@intrnl/xxhash64": "^0.1.2", + "@sapphi-red/web-noise-suppressor": "0.3.5", + "@types/less": "^3.0.6", + "@types/stylus": "^0.48.42", + "@types/tinycolor2": "^1.4.6", + "@vap/core": "0.0.12", + "@vap/shiki": "0.10.5", + "fflate": "^0.8.2", + "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", + "jsqr": "1.4.0", + "idb": "8.0.0", + "monaco-editor": "^0.52.2", + "nanoid": "^5.1.5", + "socket.io": "^4.8.1", + "usercss-meta": "^0.12.0", + "openai": "^4.30.0", + "virtual-merge": "^1.0.1" + }, + "devDependencies": { + "@electron/asar": "^3.2.10", + "@stylistic/eslint-plugin": "^4.2.0", + "@types/chrome": "^0.0.312", + "@types/diff": "^7.0.2", + "@types/lodash": "^4.17.14", + "@types/node": "^22.13.13", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@types/yazl": "^2.4.5", + "diff": "^7.0.0", + "discord-types": "^1.3.26", + "esbuild": "^0.25.1", + "eslint": "^8.57.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-react": "^7.37.3", + "eslint-plugin-simple-header": "^1.2.1", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-unused-imports": "^4.1.4", + "highlight.js": "11.11.1", + "html-minifier-terser": "^7.2.0", + "moment": "^2.22.2", + "puppeteer-core": "^24.4.0", + "standalone-electron-types": "^34.2.0", + "stylelint": "^16.17.0", + "stylelint-config-standard": "^37.0.0", + "ts-patch": "^3.3.0", + "ts-pattern": "^5.6.0", + "type-fest": "^4.41.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.28.0", + "typescript-transform-paths": "^3.5.5", + "zip-local": "^0.3.5", + "zustand": "^3.7.2" + }, + "packageManager": "bun@1.1.0", + "trustedDependencies": [ + "esbuild" + ], + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + } +} \ No newline at end of file diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 00c0b45b..c95640b2 100644 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -34,6 +34,7 @@ const defines = stringifyValues({ IS_UPDATER_DISABLED, IS_WEB: false, IS_EXTENSION: false, + IS_USERSCRIPT: false, VERSION, BUILD_TIMESTAMP }); diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index dadaf294..3be51d67 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -43,6 +43,7 @@ const commonOptions = { define: stringifyValues({ IS_WEB: true, IS_EXTENSION: false, + IS_USERSCRIPT: false, IS_STANDALONE: true, IS_DEV, IS_REPORTER, @@ -108,6 +109,7 @@ const buildConfigs = [ inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])], define: { ...commonOptions.define, + IS_USERSCRIPT: "true", window: "unsafeWindow", }, outfile: "dist/Vencord.user.js", diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index ca3eab14..061cb462 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -146,7 +146,7 @@ export const globPlugins = kind => ({ }); build.onLoad({ filter, namespace: "import-plugins" }, async () => { - const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins", "equicordplugins", "equicordplugins/_core"]; + const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins", "equicordplugins"]; let code = ""; let pluginsCode = "\n"; let metaCode = "\n"; diff --git a/scripts/generatePluginList.ts b/scripts/generatePluginList.ts index d2dd2663..e1cb18af 100644 --- a/scripts/generatePluginList.ts +++ b/scripts/generatePluginList.ts @@ -264,7 +264,7 @@ function isPluginFile({ name }: { name: string; }) { const plugins = [] as PluginData[]; - await Promise.all(["src/plugins", "src/plugins/_core", "src/equicordplugins", "src/equicordplugins/_core"].flatMap(dir => + await Promise.all(["src/plugins", "src/plugins/_core", "src/equicordplugins"].flatMap(dir => readdirSync(dir, { withFileTypes: true }) .filter(isPluginFile) .map(async dirent => { diff --git a/src/Vencord.ts b/src/Vencord.ts index b43564a2..5b954a07 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -153,7 +153,11 @@ async function init() { if (!IS_WEB && !IS_UPDATER_DISABLED) { runUpdateCheck(); - setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes + + // this tends to get really annoying, so only do this if the user has auto-update without notification enabled + if (Settings.autoUpdate && !Settings.autoUpdateNotification) { + setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes + } } if (IS_DEV) { diff --git a/src/api/MessageAccessories.tsx b/src/api/MessageAccessories.tsx index 71664e93..d2bc081e 100644 --- a/src/api/MessageAccessories.tsx +++ b/src/api/MessageAccessories.tsx @@ -48,7 +48,7 @@ export function _modifyAccessories( ) { for (const [key, accessory] of accessories.entries()) { const res = ( - + ); diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css index 98dff6df..ba8a246a 100644 --- a/src/api/Notifications/styles.css +++ b/src/api/Notifications/styles.css @@ -11,6 +11,10 @@ width: 100%; } +.visual-refresh .vc-notification-root { + background-color: var(--background-base-low); +} + .vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) { position: absolute; z-index: 2147483647; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 0ca20440..7058b5fd 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => { logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack); } + get isNoop() { + if (IS_DEV) return false; + return this.props.noop; + } + render() { if (this.state.error === NO_ERROR) return this.props.children; - if (this.props.noop) return null; + if (this.isNoop) return null; if (this.props.fallback) return ( diff --git a/src/components/ErrorCard.css b/src/components/ErrorCard.css index 5146aa03..6401c59c 100644 --- a/src/components/ErrorCard.css +++ b/src/components/ErrorCard.css @@ -4,4 +4,8 @@ border: 1px solid #e78284; border-radius: 5px; color: var(--text-normal, white); + + & a:hover { + text-decoration: underline; + } } diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 0f4eb07d..2eb7ab00 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren) { props.style.pointerEvents = "none"; props["aria-disabled"] = true; } + + props.rel ??= "noreferrer"; + return ( {props.children} diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 69a2273f..39c1f18c 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti {!!plugin.settingsAboutComponent && (
- + diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 7c2d3107..1bfaee37 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -244,7 +244,7 @@ export default function PluginSettings() { })); }, []); - const depMap = React.useMemo(() => { + const depMap = useMemo(() => { const o = {} as Record; for (const plugin in Plugins) { const deps = Plugins[plugin].dependencies; diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index 0e752efc..dc9dc7f4 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -34,6 +34,7 @@ import { useAwaiter } from "@utils/react"; import type { ThemeHeader } from "@utils/themes"; import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd"; import { usercssParse } from "@utils/themes/usercss"; +import { getStylusWebStoreUrl } from "@utils/web"; import { findLazy } from "@webpack"; import { Button, Card, Forms, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common"; import type { ComponentType, Ref, SyntheticEvent } from "react"; @@ -502,4 +503,20 @@ function ThemesTab() { ); } -export default wrapTab(ThemesTab, "Themes"); +function UserscriptThemesTab() { + return ( + + + Themes are not supported on the Userscript! + + + You can instead install themes with the Stylus extension! + + + + ); +} + +export default IS_USERSCRIPT + ? wrapTab(UserscriptThemesTab, "Themes") + : wrapTab(ThemesTab, "Themes"); diff --git a/src/equicordplugins/ABSRPC/index.tsx b/src/equicordplugins/ABSRPC/index.tsx new file mode 100644 index 00000000..48a71d0c --- /dev/null +++ b/src/equicordplugins/ABSRPC/index.tsx @@ -0,0 +1,250 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// alot of the code is from JellyfinRPC +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin, { OptionType } from "@utils/types"; +import { ApplicationAssetUtils, FluxDispatcher, Forms, showToast } from "@webpack/common"; + + +interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + +interface Activity { + state: string; + details?: string; + timestamps?: { + start?: number; + }; + assets?: ActivityAssets; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + type: number; + flags: number; +} + +interface MediaData { + name: string; + type: string; + author?: string; + series?: string; + duration?: number; + currentTime?: number; + progress?: number; + url?: string; + imageUrl?: string; + isFinished?: boolean; +} + + + +const settings = definePluginSettings({ + serverUrl: { + description: "AudioBookShelf server URL (e.g., https://abs.example.com)", + type: OptionType.STRING, + }, + username: { + description: "AudioBookShelf username", + type: OptionType.STRING, + }, + password: { + description: "AudioBookShelf password", + type: OptionType.STRING, + }, +}); + +const applicationId = "1381423044907503636"; + +const logger = new Logger("AudioBookShelfRichPresence"); + +let authToken: string | null = null; + +async function getApplicationAsset(key: string): Promise { + return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; +} + +function setActivity(activity: Activity | null) { + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity, + socketId: "ABSRPC", + }); +} + +export default definePlugin({ + name: "AudioBookShelfRichPresence", + description: "Rich presence for AudioBookShelf media server", + authors: [EquicordDevs.vmohammad], + + settingsAboutComponent: () => ( + <> + How to connect to AudioBookShelf + + Enter your AudioBookShelf server URL, username, and password to display your currently playing audiobooks as Discord Rich Presence. +

+ The plugin will automatically authenticate and fetch your listening progress. +
+ + ), + + settings, + + start() { + this.updatePresence(); + this.updateInterval = setInterval(() => { this.updatePresence(); }, 10000); + }, + + stop() { + clearInterval(this.updateInterval); + }, + + async authenticate(): Promise { + if (!settings.store.serverUrl || !settings.store.username || !settings.store.password) { + logger.warn("AudioBookShelf server URL, username, or password is not set in settings."); + showToast("AudioBookShelf RPC is not configured.", "failure", { + duration: 15000, + }); + return false; + } + + try { + const baseUrl = settings.store.serverUrl.replace(/\/$/, ""); + const url = `${baseUrl}/login`; + + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: settings.store.username, + password: settings.store.password, + }), + }); + + if (!res.ok) throw `${res.status} ${res.statusText}`; + + const data = await res.json(); + authToken = data.user?.token; + return !!authToken; + } catch (e) { + logger.error("Failed to authenticate with AudioBookShelf", e); + authToken = null; + return false; + } + }, + + async fetchMediaData(): Promise { + if (!authToken && !(await this.authenticate())) { + return null; + } + + const isPlayingNow = session => { + const now = Date.now(); + const lastUpdate = session.updatedAt; + const diffSeconds = (now - lastUpdate) / 1000; + return diffSeconds <= 30; + }; + try { + const baseUrl = settings.store.serverUrl!.replace(/\/$/, ""); + const url = `${baseUrl}/api/me/listening-sessions`; + + const res = await fetch(url, { + headers: { + "Authorization": `Bearer ${authToken}`, + }, + }); + + if (!res.ok) { + if (res.status === 401) { + authToken = null; + if (await this.authenticate()) { + return this.fetchMediaData(); + } + } + throw `${res.status} ${res.statusText}`; + } + + const { sessions } = await res.json(); + const activeSession = sessions.find((session: any) => + session.updatedAt && !session.isFinished + ); + + if (!activeSession || !isPlayingNow(activeSession)) return null; + + const { mediaMetadata: media, mediaType, duration, currentTime, libraryItemId } = activeSession; + if (!media) return null; + console.log(media); + return { + name: media.title || "Unknown", + type: mediaType || "book", + author: media.author || media.publisher, + series: media.series[0]?.name, + duration, + currentTime, + imageUrl: libraryItemId ? `${baseUrl}/api/items/${libraryItemId}/cover` : undefined, + isFinished: activeSession.isFinished || false, + }; + } catch (e) { + logger.error("Failed to query AudioBookShelf API", e); + return null; + } + }, + + async updatePresence() { + setActivity(await this.getActivity()); + }, + + async getActivity(): Promise { + const mediaData = await this.fetchMediaData(); + if (!mediaData || mediaData.isFinished) return null; + + const largeImage = mediaData.imageUrl; + console.log("Large Image URL:", largeImage); + const assets: ActivityAssets = { + large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("audiobookshelf"), + large_text: mediaData.series || mediaData.author || undefined, + }; + + const getDetails = () => { + return mediaData.name; + }; + + const getState = () => { + if (mediaData.series && mediaData.author) { + return `${mediaData.series} • ${mediaData.author}`; + } + return mediaData.author || "AudioBook"; + }; + + const timestamps = mediaData.currentTime && mediaData.duration ? { + start: Date.now() - (mediaData.currentTime * 1000), + end: Date.now() + ((mediaData.duration - mediaData.currentTime) * 1000) + } : undefined; + + return { + application_id: applicationId, + name: "AudioBookShelf", + + details: getDetails(), + state: getState(), + assets, + timestamps, + + type: 2, + flags: 1, + }; + } +}); diff --git a/src/equicordplugins/betterActivities/components/SpotifyIcon.tsx b/src/equicordplugins/betterActivities/components/SpotifyIcon.tsx deleted file mode 100644 index 9210169e..00000000 --- a/src/equicordplugins/betterActivities/components/SpotifyIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import type { SVGProps } from "react"; - -export function SpotifyIcon(props: SVGProps) { - return (); -} diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx index 6212eb33..ae5d6d1e 100644 --- a/src/equicordplugins/betterActivities/index.tsx +++ b/src/equicordplugins/betterActivities/index.tsx @@ -35,16 +35,17 @@ export default definePlugin({ patches: [ { // Patch activity icons - find: "isBlockedOrIgnored(null", + find: '"ActivityStatus"', replacement: { match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/, replace: ",$self.patchActivityList($1)]" }, predicate: () => settings.store.memberList, + all: true }, { // Show all activities in the user popout/sidebar - find: "hasAvatarForGuild(null", + find: '"UserProfilePopoutBody"', replacement: { match: /(?<=(\i)\.id\)\}\)\),(\i).*?)\(0,.{0,100}\i\.id,onClose:\i\}\)/, replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })" diff --git a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx index 9e4b7893..0a3c39bf 100644 --- a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx +++ b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx @@ -10,14 +10,13 @@ import { React, Tooltip } from "@webpack/common"; import { JSX } from "react"; import { ActivityTooltip } from "../components/ActivityTooltip"; -import { SpotifyIcon } from "../components/SpotifyIcon"; import { TwitchIcon } from "../components/TwitchIcon"; import { settings } from "../settings"; import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types"; import { cl, getApplicationIcons } from "../utils"; -// if discord one day decides to change their icon this needs to be updated -const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z"); +// Discord no longer shows an icon here by default but we use the one from the popout now here +const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"); export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null { const icons: ActivityListIcon[] = []; @@ -57,7 +56,6 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis } }; addActivityIcon("Twitch", TwitchIcon); - addActivityIcon("Spotify", SpotifyIcon); if (icons.length) { const iconStyle: IconCSSProperties = { @@ -86,7 +84,7 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis // We need to filter out custom statuses const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length; if (shouldShow) { - return ; + return ; } } diff --git a/src/equicordplugins/betterActivities/utils.tsx b/src/equicordplugins/betterActivities/utils.tsx index 4798ad44..6e3afb03 100644 --- a/src/equicordplugins/betterActivities/utils.tsx +++ b/src/equicordplugins/betterActivities/utils.tsx @@ -39,11 +39,11 @@ export function getActivityApplication(activity: Activity | null) { export function getApplicationIcons(activities: Activity[], preferSmall = false): ApplicationIcon[] { const applicationIcons: ApplicationIcon[] = []; - const applications = activities.filter(activity => activity.application_id || activity.platform); + const applications = activities.filter(activity => activity.application_id || activity.platform || activity?.id?.startsWith("spotify:")); for (const activity of applications) { - const { assets, application_id, platform } = activity; - if (!application_id && !platform) continue; + const { assets, application_id, platform, id } = activity; + if (!application_id && !platform && !id.startsWith("spotify:")) continue; if (assets) { const { small_image, small_text, large_image, large_text } = assets; @@ -59,6 +59,12 @@ export function getApplicationIcons(activities: Activity[], preferSmall = false) activity }); } + } else if (image.startsWith("spotify:")) { + const url = `https://i.scdn.co/image/${image.split(":")[1]}`; + applicationIcons.push({ + image: { src: url, alt }, + activity + }); } else { const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`; applicationIcons.push({ diff --git a/src/equicordplugins/betterAudioPlayer/style.css b/src/equicordplugins/betterAudioPlayer/style.css index e96b9338..7ce03332 100644 --- a/src/equicordplugins/betterAudioPlayer/style.css +++ b/src/equicordplugins/betterAudioPlayer/style.css @@ -7,4 +7,4 @@ pointer-events: none; z-index: 1; border: none; -} \ No newline at end of file +} diff --git a/src/equicordplugins/bypassPinPrompt/index.ts b/src/equicordplugins/bypassPinPrompt/index.ts index 0d4ac7ed..d050f723 100644 --- a/src/equicordplugins/bypassPinPrompt/index.ts +++ b/src/equicordplugins/bypassPinPrompt/index.ts @@ -9,7 +9,7 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "BypassPinPrompt", - description: "Bypass the pin prompt when pinning messages", + description: "Bypass the pin prompt when using the pin functions", authors: [EquicordDevs.thororen], patches: [ { diff --git a/src/equicordplugins/_core/equicordHelper.tsx b/src/equicordplugins/equicordHelper/index.tsx similarity index 100% rename from src/equicordplugins/_core/equicordHelper.tsx rename to src/equicordplugins/equicordHelper/index.tsx diff --git a/src/equicordplugins/equicordHelper/native.ts b/src/equicordplugins/equicordHelper/native.ts new file mode 100644 index 00000000..84887c48 --- /dev/null +++ b/src/equicordplugins/equicordHelper/native.ts @@ -0,0 +1,9 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { CspPolicies, MediaScriptsAndCssSrc } from "@main/csp"; + +CspPolicies["*"] = MediaScriptsAndCssSrc; diff --git a/src/equicordplugins/equicordToolbox/index.tsx b/src/equicordplugins/equicordToolbox/index.tsx index 9326c189..59876492 100644 --- a/src/equicordplugins/equicordToolbox/index.tsx +++ b/src/equicordplugins/equicordToolbox/index.tsx @@ -147,7 +147,7 @@ function VencordPopoutButton() { function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) { children.splice( children.length - 1, 0, - + ); diff --git a/src/equicordplugins/fixFileExtensions/index.tsx b/src/equicordplugins/fixFileExtensions/index.tsx index 85d15030..e4664a7e 100644 --- a/src/equicordplugins/fixFileExtensions/index.tsx +++ b/src/equicordplugins/fixFileExtensions/index.tsx @@ -24,8 +24,6 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta return acc; }, {} as Record); -type ExtUpload = Upload & { fixExtension?: boolean; }; - export default definePlugin({ name: "FixFileExtensions", authors: [EquicordDevs.thororen], @@ -34,15 +32,21 @@ export default definePlugin({ patches: [ // Taken from AnonymiseFileNames { - find: 'type:"UPLOAD_START"', - replacement: { - match: /await \i\.uploadFiles\((\i),/, - replace: "$1.forEach($self.fixExt),$&" - }, + find: "async uploadFiles(", + replacement: [ + { + match: /async uploadFiles\((\i),\i\){/, + replace: "$&$1.forEach($self.fixExt);" + }, + { + match: /async uploadFilesSimple\((\i)\){/, + replace: "$&$1.forEach($self.fixExt);" + } + ], predicate: () => !Settings.plugins.AnonymiseFileNames.enabled, }, ], - fixExt(upload: ExtUpload) { + fixExt(upload: Upload) { const file = upload.filename; const tarMatch = tarExtMatcher.exec(file); const extIdx = tarMatch?.index ?? file.lastIndexOf("."); diff --git a/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts b/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts index 32d836b4..e36d40de 100644 --- a/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts +++ b/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts @@ -24,7 +24,7 @@ export default async ( }: FurudoSettings, repliedMessage?: Message ): Promise => { - const completion = await fetch("http://localhost:11434/api/chat", { + const completion = await fetch("http://127.0.0.1:11434/api/chat", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/equicordplugins/jellyfinRichPresence/index.tsx b/src/equicordplugins/jellyfinRichPresence/index.tsx new file mode 100644 index 00000000..aa27bedf --- /dev/null +++ b/src/equicordplugins/jellyfinRichPresence/index.tsx @@ -0,0 +1,215 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// alot of the code is from LastFMRichPresence +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin, { OptionType } from "@utils/types"; +import { ApplicationAssetUtils, FluxDispatcher, Forms, showToast } from "@webpack/common"; + + +interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + +interface Activity { + state: string; + details?: string; + timestamps?: { + start?: number; + }; + assets?: ActivityAssets; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + type: number; + flags: number; +} + +interface MediaData { + name: string; + type: string; + artist?: string; + album?: string; + seriesName?: string; + seasonNumber?: number; + episodeNumber?: number; + year?: number; + url?: string; + imageUrl?: string; + duration?: number; + position?: number; +} + + + +const settings = definePluginSettings({ + serverUrl: { + description: "Jellyfin server URL (e.g., https://jellyfin.example.com)", + type: OptionType.STRING, + }, + apiKey: { + description: "Jellyfin API key obtained from your Jellyfin administration dashboard", + type: OptionType.STRING, + }, + userId: { + description: "Jellyfin user ID obtained from your user profile URL", + type: OptionType.STRING, + }, +}); + +const applicationId = "1381368130164625469"; + +const logger = new Logger("JellyfinRichPresence"); + +async function getApplicationAsset(key: string): Promise { + return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; +} + +function setActivity(activity: Activity | null) { + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity, + socketId: "Jellyfin", + }); +} + +export default definePlugin({ + name: "JellyfinRichPresence", + description: "Rich presence for Jellyfin media server", + authors: [EquicordDevs.vmohammad], + + settingsAboutComponent: () => ( + <> + How to get an API key + + An API key is required to fetch your current media. To get one, go to your + Jellyfin dashboard, navigate to Administration {">"} API Keys and + create a new API key.

+ + You'll also need your User ID, which can be found in the url of your user profile page. +
+ + ), + + settings, + + start() { + this.updatePresence(); + this.updateInterval = setInterval(() => { this.updatePresence(); }, 10000); + }, + + stop() { + clearInterval(this.updateInterval); + }, + + async fetchMediaData(): Promise { + if (!settings.store.serverUrl || !settings.store.apiKey || !settings.store.userId) { + logger.warn("Jellyfin server URL, API key, or user ID is not set in settings."); + showToast("JellyfinRPC is not configured.", "failure", { + duration: 15000, + }); + return null; + } + + try { + const baseUrl = settings.store.serverUrl.replace(/\/$/, ""); + const url = `${baseUrl}/Sessions?api_key=${settings.store.apiKey}`; + + const res = await fetch(url); + if (!res.ok) throw `${res.status} ${res.statusText}`; + + const sessions = await res.json(); + const userSession = sessions.find((session: any) => + session.UserId === settings.store.userId && session.NowPlayingItem + ); + + if (!userSession || !userSession.NowPlayingItem) return null; + + const item = userSession.NowPlayingItem; + const playState = userSession.PlayState; + + if (playState?.IsPaused) return null; + + const imageUrl = item.ImageTags?.Primary + ? `${baseUrl}/Items/${item.Id}/Images/Primary` + : undefined; + + return { + name: item.Name || "Unknown", + type: item.Type, + artist: item.Artists?.[0] || item.AlbumArtist, + album: item.Album, + seriesName: item.SeriesName, + seasonNumber: item.ParentIndexNumber, + episodeNumber: item.IndexNumber, + year: item.ProductionYear, + url: `${baseUrl}/web/#!/details?id=${item.Id}`, + imageUrl, + duration: item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined, + position: playState?.PositionTicks ? Math.floor(playState.PositionTicks / 10000000) : undefined + }; + } catch (e) { + logger.error("Failed to query Jellyfin API", e); + return null; + } + }, + + async updatePresence() { + setActivity(await this.getActivity()); + }, + + async getActivity(): Promise { + const mediaData = await this.fetchMediaData(); + if (!mediaData) return null; + + const largeImage = mediaData.imageUrl; + const assets: ActivityAssets = { + large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("jellyfin"), + large_text: mediaData.album || mediaData.seriesName || undefined, + }; + + const getDetails = () => { + if (mediaData.type === "Episode" && mediaData.seriesName) { + return mediaData.name; + } + return mediaData.name; + }; + + const getState = () => { + if (mediaData.type === "Episode" && mediaData.seriesName) { + const season = mediaData.seasonNumber ? `S${mediaData.seasonNumber}` : ""; + const episode = mediaData.episodeNumber ? `E${mediaData.episodeNumber}` : ""; + return `${mediaData.seriesName} ${season}${episode}`.trim(); + } + return mediaData.artist || (mediaData.year ? `(${mediaData.year})` : undefined); + }; + + const timestamps = mediaData.position && mediaData.duration ? { + start: Date.now() - (mediaData.position * 1000), + end: Date.now() + ((mediaData.duration - mediaData.position) * 1000) + } : undefined; + + return { + application_id: applicationId, + name: "Jellyfin", + + details: getDetails(), + state: getState() || "something", + assets, + timestamps, + + type: 3, + flags: 1, + }; + } +}); diff --git a/src/equicordplugins/questCompleter.discordDesktop/index.tsx b/src/equicordplugins/questCompleter.discordDesktop/index.tsx index 19a13533..549ea700 100644 --- a/src/equicordplugins/questCompleter.discordDesktop/index.tsx +++ b/src/equicordplugins/questCompleter.discordDesktop/index.tsx @@ -51,40 +51,40 @@ async function openCompleteQuestUI() { const applicationId = quest.config.application.id; const applicationName = quest.config.application.name; const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null); + const icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`; // @ts-ignore const secondsNeeded = quest.config.taskConfig.tasks[taskName].target; // @ts-ignore - const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; - const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`; + let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; if (taskName === "WATCH_VIDEO") { - const tolerance = 2, speed = 10; - const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000); - const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded); + const maxFuture = 10, speed = 7, interval = 1; + const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime(); const fn = async () => { - for (let i = startingPoint; i <= secondsNeeded; i += speed) { - try { - await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } }); - } catch (ex) { - console.log("Failed to send increment of", i, ex); + while (true) { + const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture; + const diff = maxAllowed - secondsDone; + const timestamp = secondsDone + speed; + if (diff >= speed) { + await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } }); + secondsDone = Math.min(secondsNeeded, timestamp); } - await new Promise(resolve => setTimeout(resolve, tolerance * 1000)); - } - if ((secondsNeeded - secondsDone) % speed !== 0) { - await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } }); - showNotification({ - title: `${applicationName} - Quest Completer`, - body: "Quest Completed.", - icon: icon, - }); + if (timestamp >= secondsNeeded) { + break; + } + await new Promise(resolve => setTimeout(resolve, interval * 1000)); } + showNotification({ + title: `${applicationName} - Quest Completer`, + body: "Quest Completed.", + icon: icon, + }); }; fn(); showNotification({ title: `${applicationName} - Quest Completer`, - body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`, + body: `Spoofing video for ${applicationName}.`, icon: icon, }); - console.log(`Spoofing video for ${applicationName}.`); } else if (taskName === "PLAY_ON_DESKTOP") { RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => { const appData = res.body[0]; diff --git a/src/equicordplugins/streamingCodecDisabler/index.ts b/src/equicordplugins/streamingCodecDisabler/index.ts new file mode 100644 index 00000000..f26113e9 --- /dev/null +++ b/src/equicordplugins/streamingCodecDisabler/index.ts @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings, Settings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findStoreLazy } from "@webpack"; + +let mediaEngine = findStoreLazy("MediaEngineStore"); + +const originalCodecStatuses: { + AV1: boolean, + H265: boolean, + H264: boolean; +} = { + AV1: true, + H265: true, + H264: true +}; + +const settings = definePluginSettings({ + disableAv1Codec: { + description: "Make Discord not consider using AV1 for streaming.", + type: OptionType.BOOLEAN, + default: false + }, + disableH265Codec: { + description: "Make Discord not consider using H265 for streaming.", + type: OptionType.BOOLEAN, + default: false + }, + disableH264Codec: { + description: "Make Discord not consider using H264 for streaming.", + type: OptionType.BOOLEAN, + default: false + }, +}); + +export default definePlugin({ + name: "StreamingCodecDisabler", + description: "Disable codecs for streaming of your choice", + authors: [EquicordDevs.davidkra230], + settings, + + patches: [ + { + find: "setVideoBroadcast(this.shouldConnectionBroadcastVideo", + replacement: { + match: /setGoLiveSource\(.,.\)\{/, + replace: "$&$self.updateDisabledCodecs();" + }, + } + ], + + async updateDisabledCodecs() { + mediaEngine.setAv1Enabled(originalCodecStatuses.AV1 && !Settings.plugins.StreamingCodecDisabler.disableAv1Codec); + mediaEngine.setH265Enabled(originalCodecStatuses.H265 && !Settings.plugins.StreamingCodecDisabler.disableH265Codec); + mediaEngine.setH264Enabled(originalCodecStatuses.H264 && !Settings.plugins.StreamingCodecDisabler.disableH264Codec); + }, + + async start() { + mediaEngine = mediaEngine.getMediaEngine(); + const options = Object.keys(originalCodecStatuses); + // [{"codec":"","decode":false,"encode":false}] + const CodecCapabilities = JSON.parse(await new Promise(res => mediaEngine.getCodecCapabilities(res))); + CodecCapabilities.forEach((codec: { codec: string; encode: boolean; }) => { + if (options.includes(codec.codec)) { + originalCodecStatuses[codec.codec] = codec.encode; + } + }); + }, + + async stop() { + mediaEngine.setAv1Enabled(originalCodecStatuses.AV1); + mediaEngine.setH265Enabled(originalCodecStatuses.H265); + mediaEngine.setH264Enabled(originalCodecStatuses.H264); + } +}); diff --git a/src/equicordplugins/timezones/database.tsx b/src/equicordplugins/timezones/database.tsx index f382c917..a313828e 100644 --- a/src/equicordplugins/timezones/database.tsx +++ b/src/equicordplugins/timezones/database.tsx @@ -8,7 +8,6 @@ import { openModal } from "@utils/index"; import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common"; const databaseTimezones: Record = {}; - const DOMAIN = "https://timezone.creations.works"; const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`; const CLIENT_ID = "1377021506810417173"; @@ -26,7 +25,6 @@ export async function loadDatabaseTimezones(): Promise { const res = await fetch(`${DOMAIN}/list`, { headers: { Accept: "application/json" } }); - if (res.ok) { const json = await res.json(); for (const id in json) { @@ -34,10 +32,8 @@ export async function loadDatabaseTimezones(): Promise { value: json[id]?.timezone ?? null }; } - return true; } - return false; } catch (e) { console.error("Failed to fetch timezones list:", e); @@ -45,30 +41,93 @@ export async function loadDatabaseTimezones(): Promise { } } -export async function setTimezone(timezone: string): Promise { - const res = await fetch(`${DOMAIN}/set?timezone=${encodeURIComponent(timezone)}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json" - }, - credentials: "include" - }); +async function checkAuthentication(): Promise { + try { + const res = await fetch(`${DOMAIN}/me`, { + credentials: "include", + headers: { Accept: "application/json" } + }); + return res.ok; + } catch (e) { + console.error("Failed to check authentication:", e); + return false; + } +} - return res.ok; +export async function setTimezone(timezone: string): Promise { + const isAuthenticated = await checkAuthentication(); + + if (!isAuthenticated) { + return new Promise(resolve => { + authModal(() => { + setTimezoneInternal(timezone).then(resolve); + }); + }); + } + + return setTimezoneInternal(timezone); +} + +async function setTimezoneInternal(timezone: string): Promise { + const formData = new URLSearchParams(); + formData.append("timezone", timezone); + + try { + const res = await fetch(`${DOMAIN}/set`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json" + }, + credentials: "include", + body: formData + }); + + if (!res.ok) { + const error = await res.json().catch(() => ({ message: "Unknown error" })); + showToast(error.message || "Failed to set timezone", Toasts.Type.FAILURE); + return false; + } + + showToast("Timezone updated successfully!", Toasts.Type.SUCCESS); + return true; + } catch (e) { + console.error("Error setting timezone:", e); + showToast("Failed to set timezone", Toasts.Type.FAILURE); + return false; + } } export async function deleteTimezone(): Promise { - const res = await fetch(`${DOMAIN}/delete`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json" - }, - credentials: "include" - }); + const isAuthenticated = await checkAuthentication(); - return res.ok; + if (!isAuthenticated) { + showToast("You must be logged in to delete your timezone", Toasts.Type.FAILURE); + return false; + } + + try { + const res = await fetch(`${DOMAIN}/delete`, { + method: "DELETE", + headers: { + Accept: "application/json" + }, + credentials: "include" + }); + + if (!res.ok) { + const error = await res.json().catch(() => ({ message: "Unknown error" })); + showToast(error.message || "Failed to delete timezone", Toasts.Type.FAILURE); + return false; + } + + showToast("Timezone deleted successfully!", Toasts.Type.SUCCESS); + return true; + } catch (e) { + console.error("Error deleting timezone:", e); + showToast("Failed to delete timezone", Toasts.Type.FAILURE); + return false; + } } export function authModal(callback?: () => void) { @@ -83,21 +142,17 @@ export function authModal(callback?: () => void) { cancelCompletesFlow={false} callback={async (res: any) => { if (!res || !res.location) return; - try { const url = new URL(res.location); - const r = await fetch(url, { credentials: "include", headers: { Accept: "application/json" } }); - const json = await r.json(); if (!r.ok) { showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE); return; } - showToast("Authorization successful!", Toasts.Type.SUCCESS); callback?.(); } catch (e) { diff --git a/src/equicordplugins/timezones/index.tsx b/src/equicordplugins/timezones/index.tsx index c16b432d..2871c342 100644 --- a/src/equicordplugins/timezones/index.tsx +++ b/src/equicordplugins/timezones/index.tsx @@ -17,7 +17,7 @@ import { findByPropsLazy } from "@webpack"; import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common"; import { Message, User } from "discord-types/general"; -import { authModal, deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database"; +import { deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database"; import { SetTimezoneModal } from "./TimezoneModal"; export let timezones: Record = {}; @@ -68,9 +68,7 @@ export const settings = definePluginSettings({ type: OptionType.COMPONENT, component: () => ( @@ -83,11 +81,19 @@ export const settings = definePluginSettings({ component: () => ( - ))} -
- - ); - })} - - - - ); -} - diff --git a/src/equicordplugins/wallpaperFree/components/util.tsx b/src/equicordplugins/wallpaperFree/components/util.tsx index 763d8882..6d8bba8f 100644 --- a/src/equicordplugins/wallpaperFree/components/util.tsx +++ b/src/equicordplugins/wallpaperFree/components/util.tsx @@ -6,13 +6,9 @@ import { openModal } from "@utils/modal"; import { makeCodeblock } from "@utils/text"; -import { findByCodeLazy, findStoreLazy } from "@webpack"; import { Button, FluxDispatcher, Parser } from "@webpack/common"; -import { SetCustomWallpaperModal, SetDiscordWallpaperModal } from "./modal"; - -export const ChatWallpaperStore = findStoreLazy("ChatWallpaperStore"); -export const fetchWallpapers = findByCodeLazy('type:"FETCH_CHAT_WALLPAPERS_SUCCESS"'); +import { SetWallpaperModal } from "./modal"; export function GlobalDefaultComponent() { const setGlobal = (url?: string) => { @@ -26,13 +22,8 @@ export function GlobalDefaultComponent() { return ( <> - - + openModal(props => ); + }}>Set a global wallpaper