Compare commits

...

19 Commits

Author SHA1 Message Date
Vendicated
462f191051 Bump to v1.1.4 2023-04-02 04:26:05 +02:00
V
6960a439c9 Add Notification log (#745) 2023-04-01 02:47:49 +02:00
Vendicated
4dff1c5bd5 RelationShipNotifier: Delay by 5s to fix false positives 2023-03-31 17:17:50 +02:00
nick
2c8ebdce7d feat(plugin): RelationshipNotifier (#450)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-31 05:07:35 +00:00
Nuckyz
dae7cb67ef Fix IgnoreActivities broken patch (#743) 2023-03-31 04:11:15 +00:00
Berlin
081b01b667 feat(plugin): Wikisearch (#585)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-31 04:09:19 +00:00
Vendicated
5340ea7ba0 Add back window transparency with temporary unsafe settings key 2023-03-31 05:59:45 +02:00
Vendicated
84a649a671 docs: fix ToC 2023-03-31 05:56:08 +02:00
Vendicated
efd9927696 Fix broken plugins 2023-03-31 05:55:25 +02:00
V
c86a34a15d Update 1_INSTALLING.md 2023-03-31 05:30:45 +02:00
Vendicated
ff16513f21 Fix onHeadersReceived clashes when using OpenAsar (fix github raw styles) 2023-03-31 01:18:57 +02:00
Vendicated
906c265aea FakeNitro: Fix fake emote rendering incorrectly in thread previews 2023-03-31 00:15:51 +02:00
Vendicated
708c16176b Remove transparency feature
This not only causes incredibly confusion among users because they
expect it to work without themes, it also causes freezes/whitescreens
for some users. Thus, this feature is disabled for now until someone
contributes a fix!
2023-03-30 23:48:26 +02:00
whqwert
035d1e24b2 feat(SpotifyControls): Fix background color for built-in themes (#731)
Co-authored-by: V <vendicated@riseup.net>
2023-03-30 17:09:04 +02:00
Vendicated
48e9b1be7a new Plugin: GifPaste - Insert Gif links instead of sending 2023-03-30 15:58:20 +02:00
Vendicated
6acdaf207d NoTrack: Update description & authors 2023-03-30 01:41:18 +02:00
Vendicated
9d41b360c9 Fix NoTrack 2023-03-30 01:35:42 +02:00
Vendicated
12cbd73e7f SpotifyControls: Add right click menus to title/album/artists 2023-03-30 01:29:34 +02:00
Phil
420b068094 Fix makeProxy returning stale proxies after assigning objects (#722) 2023-03-28 18:26:57 +00:00
34 changed files with 1039 additions and 149 deletions

View File

@ -13,12 +13,6 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
- [Installing Vencord](#installing-vencord)
- [Updating Vencord](#updating-vencord)
- [Uninstalling Vencord](#uninstalling-vencord)
- [Manually Installing Vencord](#manually-installing-vencord)
- [On Windows](#on-windows)
- [On Linux](#on-linux)
- [On MacOS](#on-macos)
- [Manual Patching](#manual-patching)
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
## Dependencies
@ -27,11 +21,9 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
## Installing Vencord
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
Install `pnpm`:
> :exclamation: This next command may need to be run as admin/sudo depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
```shell
npm i -g pnpm
@ -103,102 +95,4 @@ Simply run:
pnpm uninject
```
The above command may ask you to also run:
```shell
pnpm install --frozen-lockfile
pnpm uninject
```
## Manually Installing Vencord
- [Windows](#on-windows)
- [Linux](#on-linux)
- [MacOS](#on-macos)
### On Windows
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
Now follow the instructions at [Manual Patching](#manual-patching)
### On Linux
The Discord folder is usually in one of the following paths:
- /usr/share
- /usr/lib64
- /opt
- /home/$USER/.local/share
If you use flatpak, it will usually be in one of the following paths:
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
You will need to give flatpak access to vencord with one of the following commands:
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
If Discord flatpak install is in /home/:
```shell
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
```
If Discord flatpak install not in /home/:
```shell
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
```
Now follow the instructions at [Manual Patching](#manual-patching)
### On MacOS
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
Go to the `Contents/Resources` folder.
Now follow the instructions at [Manual Patching](#manual-patching)
### Manual Patching
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
Make a new folder in `resources` called `app`. In here, we will make two files:
`package.json` and `index.js`
In `index.js`:
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
```js
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
```
And in `package.json`:
```json
{ "name": "discord", "main": "index.js" }
```
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
### Manually Uninstalling Vencord
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View File

@ -1,9 +1,8 @@
{
"name": "vencord",
"private": "true",
"version": "1.1.3",
"version": "1.1.4",
"description": "The cutest Discord client mod",
"keywords": [ ],
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
"url": "https://github.com/Vendicated/Vencord/issues"
@ -34,11 +33,13 @@
"dependencies": {
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"fflate": "^0.7.4"
"fflate": "^0.7.4",
"nanoid": "^4.0.2"
},
"devDependencies": {
"@types/diff": "^5.0.2",
"@types/lodash": "^4.14.191",
"@types/nanoid": "^3.0.0",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",

16
pnpm-lock.yaml generated
View File

@ -11,6 +11,7 @@ patchedDependencies:
specifiers:
'@types/diff': ^5.0.2
'@types/lodash': ^4.14.191
'@types/nanoid': ^3.0.0
'@types/node': ^18.11.18
'@types/react': ^18.0.27
'@types/react-dom': ^18.0.10
@ -31,6 +32,7 @@ specifiers:
fflate: ^0.7.4
highlight.js: 10.6.0
moment: ^2.29.4
nanoid: ^4.0.2
puppeteer-core: ^19.6.0
standalone-electron-types: ^1.0.0
stylelint: ^14.16.1
@ -43,10 +45,12 @@ dependencies:
'@vap/core': 0.0.12
'@vap/shiki': 0.10.3
fflate: 0.7.4
nanoid: 4.0.2
devDependencies:
'@types/diff': 5.0.2
'@types/lodash': 4.14.191
'@types/nanoid': 3.0.0
'@types/node': 18.11.18
'@types/react': 18.0.27
'@types/react-dom': 18.0.10
@ -417,6 +421,13 @@ packages:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
dev: true
/@types/nanoid/3.0.0:
resolution: {integrity: sha512-UXitWSmXCwhDmAKe7D3hNQtQaHeHt5L8LO1CB8GF8jlYVzOv5cBWDNqiJ+oPEWrWei3i3dkZtHY/bUtd0R/uOQ==}
deprecated: This is a stub types definition. nanoid provides its own type definitions, so you do not need this installed.
dependencies:
nanoid: 4.0.2
dev: true
/@types/node/18.11.18:
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
dev: true
@ -2245,6 +2256,11 @@ packages:
hasBin: true
dev: true
/nanoid/4.0.2:
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
engines: {node: ^14 || ^16 || >=18}
hasBin: true
/nanomatch/1.2.13:
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
engines: {node: '>=0.10.0'}

View File

@ -36,7 +36,7 @@ const commonOptions = {
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
external: ["plugins", "git-hash"],
external: ["plugins", "git-hash", "/assets/*"],
plugins: [
globPlugins,
...commonOpts.plugins,

View File

@ -193,7 +193,7 @@ export const commonOpts = {
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote"],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",

View File

@ -54,6 +54,7 @@ async function init() {
title: "Vencord has been updated!",
body: "Click here to restart",
permanent: true,
noPersist: true,
onClick() {
if (needsFullRestart)
window.DiscordNative.app.relaunch();
@ -69,6 +70,7 @@ async function init() {
title: "A Vencord update is available!",
body: "Click here to view the update",
permanent: true,
noPersist: true,
onClick() {
SettingsRouter.open("VencordUpdater");
}

View File

@ -20,6 +20,7 @@ import "./styles.css";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc";
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { NotificationData } from "./Notifications";
@ -33,8 +34,10 @@ export default ErrorBoundary.wrap(function NotificationComponent({
onClick,
onClose,
image,
permanent
}: NotificationData) {
permanent,
className,
dismissOnClick
}: NotificationData & { className?: string; }) {
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
@ -61,11 +64,12 @@ export default ErrorBoundary.wrap(function NotificationComponent({
return (
<button
className="vc-notification-root"
className={classes("vc-notification-root", className)}
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={() => {
onClose!();
onClick?.();
if (dismissOnClick !== false)
onClose!();
}}
onContextMenu={e => {
e.preventDefault();

View File

@ -23,6 +23,7 @@ import type { ReactNode } from "react";
import type { Root } from "react-dom/client";
import NotificationComponent from "./NotificationComponent";
import { persistNotification } from "./notificationLog";
const NotificationQueue = new Queue();
@ -56,6 +57,10 @@ export interface NotificationData {
color?: string;
/** Whether this notification should not have a timeout */
permanent?: boolean;
/** Whether this notification should not be persisted in the Notification Log */
noPersist?: boolean;
/** Whether this notification should be dismissed when clicked (defaults to true) */
dismissOnClick?: boolean;
}
function _showNotification(notification: NotificationData, id: number) {
@ -86,6 +91,8 @@ export async function requestPermission() {
}
export async function showNotification(data: NotificationData) {
persistNotification(data);
if (shouldBeNative() && await requestPermission()) {
const { title, body, icon, image, onClick = null, onClose = null } = data;
const n = new Notification(title, {

View File

@ -0,0 +1,203 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import { useAwaiter } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
import NotificationComponent from "./NotificationComponent";
import type { NotificationData } from "./Notifications";
interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
timestamp: number;
id: string;
}
const KEY = "notification-log";
const getLog = async () => {
const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
return log ?? [];
};
const cl = classNameFactory("vc-notification-log-");
const signals = new Set<DispatchWithoutAction>();
export async function persistNotification(notification: NotificationData) {
if (notification.noPersist) return;
const limit = Settings.notifications.logLimit;
if (limit === 0) return;
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
const log = old ?? [];
// Omit stuff we don't need
const {
onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
...pureNotification
} = notification;
log.unshift({
...pureNotification,
timestamp: Date.now(),
id: nanoid()
});
if (log.length > limit && limit !== 200)
log.length = limit;
return log;
});
signals.forEach(x => x());
}
export async function deleteNotification(timestamp: number) {
const log = await getLog();
const index = log.findIndex(x => x.timestamp === timestamp);
if (index === -1) return;
log.splice(index, 1);
await DataStore.set(KEY, log);
signals.forEach(x => x());
}
export function useLogs() {
const [signal, setSignal] = useReducer(x => x + 1, 0);
useEffect(() => {
signals.add(setSignal);
return () => void signals.delete(setSignal);
}, []);
const [log, _, pending] = useAwaiter(getLog, {
fallbackValue: [],
deps: [signal]
});
return [log, pending] as const;
}
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
const [removing, setRemoving] = useState(false);
const ref = React.useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current!;
const setHeight = () => {
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
div.style.height = `${div.clientHeight}px`;
};
setHeight();
}, []);
return (
<div className={cl("wrapper", { removing })} ref={ref}>
<NotificationComponent
{...data}
permanent={true}
dismissOnClick={false}
onClose={() => {
if (removing) return;
setRemoving(true);
setTimeout(() => deleteNotification(data.timestamp), 200);
}}
richBody={
<div className={cl("body")}>
{data.body}
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>
</div>
);
}
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
if (!log.length && !pending)
return (
<div className={cl("container")}>
<div className={cl("empty")} />
<Forms.FormText style={{ textAlign: "center" }}>
No notifications yet
</Forms.FormText>
</div>
);
return (
<div className={cl("container")}>
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
</div>
);
}
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
const [log, pending] = useLogs();
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent>
<NotificationLog log={log} pending={pending} />
</ModalContent>
<ModalFooter>
<Button
disabled={log.length === 0}
onClick={() => {
Alerts.show({
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
async onConfirm() {
await DataStore.set(KEY, []);
signals.forEach(x => x());
},
confirmText: "Do it!",
confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</ModalFooter>
</ModalRoot>
);
}
export function openNotificationLogModal() {
const key = openModal(modalProps => (
<LogModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
}

View File

@ -3,16 +3,20 @@
all: unset;
display: flex;
flex-direction: column;
width: 25vw;
min-height: 10vh;
color: var(--text-normal);
background-color: var(--background-secondary-alt);
position: absolute;
z-index: 2147483647;
right: 1rem;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
width: 100%;
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
position: absolute;
z-index: 2147483647;
right: 1rem;
width: 25vw;
min-height: 10vh;
}
.vc-notification {
@ -72,3 +76,47 @@
.vc-notification-img {
width: 100%;
}
.vc-notification-log-empty {
height: 218px;
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
margin-bottom: 40px;
}
.vc-notification-log-container {
display: flex;
flex-direction: column;
padding: 1em;
overflow: hidden;
}
.vc-notification-log-wrapper {
transition: 200ms ease;
transition-property: height, opacity;
}
.vc-notification-log-wrapper:not(:last-child) {
margin-bottom: 1em;
}
.vc-notification-log-removing {
height: 0 !important;
opacity: 0;
margin-bottom: 1em;
}
.vc-notification-log-body {
display: flex;
flex-direction: column;
}
.vc-notification-log-timestamp {
margin-left: auto;
font-size: 0.8em;
font-weight: lighter;
}
.vc-notification-log-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}

View File

@ -47,6 +47,7 @@ export interface Settings {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
logLimit: number;
};
}
@ -66,7 +67,8 @@ const DefaultSettings: Settings = {
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused"
useNative: "not-focused",
logLimit: 50
}
};
@ -133,6 +135,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
target[p] = v;
// Call any listeners that are listening to a setting of this path
const setPath = `${path}${path && "."}${p}`;
delete proxyCache[setPath];
for (const subscription of subscriptions) {
if (!subscription._path || subscription._path === setPath) {
subscription(v, setPath);

View File

@ -17,6 +17,7 @@
*/
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
@ -72,7 +73,7 @@ function VencordSettings() {
title: "Use Windows' native title bar instead of Discord's custom one",
note: "Requires a full restart"
}),
!IS_WEB && {
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
key: "transparent",
title: "Enable window transparency",
note: "Requires a full restart"
@ -165,7 +166,7 @@ function VencordSettings() {
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
] satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => notifSettings.useNative = v}
isSelected={v => v === notifSettings.useNative}
@ -179,7 +180,7 @@ function VencordSettings() {
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
] satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
select={v => notifSettings.position = v}
isSelected={v => v === notifSettings.position}
serialize={identity}
@ -198,6 +199,29 @@ function VencordSettings() {
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={notifSettings.logLimit}
onValueChange={v => notifSettings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
<Button
onClick={openNotificationLogModal}
disabled={notifSettings.logLimit === 0}
>
Open Notification Log
</Button>
</React.Fragment>
);
}

View File

@ -17,7 +17,7 @@
*/
import { onceDefined } from "@utils/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import electron, { app, BrowserWindowConstructorOptions, Menu, protocol, session } from "electron";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
@ -83,7 +83,8 @@ if (!process.argv.includes("--vanilla")) {
delete options.frame;
}
if (settings.transparent) {
// This causes electron to freeze / white screen for some people
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
@ -116,10 +117,10 @@ if (!process.argv.includes("--vanilla")) {
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
electron.app.whenReady().then(() => {
app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
@ -175,7 +176,7 @@ if (!process.argv.includes("--vanilla")) {
}
}
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
@ -187,6 +188,11 @@ if (!process.argv.includes("--vanilla")) {
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
});
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");

View File

@ -22,12 +22,12 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish, Devs.Ven],
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
replace: (m, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");

View File

@ -78,6 +78,7 @@ export default definePlugin({
color: "#eed202",
title: "Discord has crashed!",
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
noPersist: true,
});
} catch { }
@ -111,6 +112,7 @@ export default definePlugin({
color: "#eed202",
title: "Discord has crashed!",
body: "Attempting to recover...",
noPersist: true,
});
} catch { }
}

View File

@ -116,7 +116,8 @@ function initWs(isManual = false) {
showNotification({
title: "Dev Companion Error",
body: (e as ErrorEvent).message || "No Error Message",
color: "var(--status-danger, red)"
color: "var(--status-danger, red)",
noPersist: true,
});
});
@ -128,7 +129,8 @@ function initWs(isManual = false) {
showNotification({
title: "Dev Companion Disconnected",
body: e.reason || "No Reason provided",
color: "var(--status-danger, red)"
color: "var(--status-danger, red)",
noPersist: true,
});
});

View File

@ -204,7 +204,7 @@ export default definePlugin({
},
{
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content});`
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content},arguments[2]?.formatInline);`
}
]
},
@ -333,7 +333,7 @@ export default definePlugin({
EmojiComponent: null as any,
patchFakeNitroEmojis(content: Array<any>) {
patchFakeNitroEmojis(content: Array<any>, inline: boolean) {
if (!this.EmojiComponent) return content;
const newContent: Array<any> = [];
@ -353,7 +353,7 @@ export default definePlugin({
newContent.push((
<this.EmojiComponent node={{
type: "customEmoji",
jumboable: content.length === 1,
jumboable: !inline && content.length === 1,
animated: fakeNitroMatch[2] === "gif",
name: ":FakeNitroEmoji:",
emojiId: fakeNitroMatch[1]

47
src/plugins/gifPaste.ts Normal file
View File

@ -0,0 +1,47 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { filters, findLazy, mapMangledModuleLazy } from "@webpack";
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', {
close: filters.byCode("activeView:null", "setState")
});
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
export default definePlugin({
name: "GifPaste",
description: "Makes picking a gif in the gif picker insert a link into the chatbox instead of instantly sending it",
authors: [Devs.Ven],
patches: [{
find: ".handleSelectGIF=",
replacement: {
match: /\.handleSelectGIF=function.+?\{/,
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);"
}
}],
handleSelect(gif?: { url: string; }) {
if (gif) {
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: gif.url + " " });
ExpressionPickerState.close();
}
}
});

View File

@ -147,8 +147,8 @@ export default definePlugin({
{
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: {
match: /!(\i)\|\|(null==\i\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => ""
match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false"
+ `${restWithoutPlatformCheck}`
+ `(${platformCheck}?${children}:[])`
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`

View File

@ -21,8 +21,8 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "NoTrack",
description: "Disable Discord's tracking and crash reporting",
authors: [Devs.Cyn],
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz],
required: true,
patches: [
{
@ -35,8 +35,8 @@ export default definePlugin({
{
find: "window.DiscordSentry=",
replacement: {
match: /window\.DiscordSentry=function.+\}\(\)/,
replace: "",
match: /^.+$/,
replace: "()=>{}",
}
},
{

View File

@ -63,8 +63,8 @@ export default definePlugin({
{
find: ".Messages.USER_POPOUT_PRONOUNS",
replacement: {
match: /\i\.\i\.useExperiment\({}\)\.showPronouns/,
replace: "true"
match: /\.showPronouns/,
replace: ".showPronouns||true"
}
}
],

View File

@ -0,0 +1,40 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { FluxEvents } from "@webpack/types";
import { onChannelDelete, onGuildDelete, onRelationshipRemove } from "./functions";
import { syncFriends, syncGroups, syncGuilds } from "./utils";
export const FluxHandlers: Partial<Record<FluxEvents, Array<(data: any) => void>>> = {
GUILD_CREATE: [syncGuilds],
GUILD_DELETE: [onGuildDelete],
CHANNEL_CREATE: [syncGroups],
CHANNEL_DELETE: [onChannelDelete],
RELATIONSHIP_ADD: [syncFriends],
RELATIONSHIP_UPDATE: [syncFriends],
RELATIONSHIP_REMOVE: [syncFriends, onRelationshipRemove]
};
export function forEachEvent(fn: (event: FluxEvents, handler: (data: any) => void) => void) {
for (const event in FluxHandlers) {
for (const cb of FluxHandlers[event]) {
fn(event as FluxEvents, cb);
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { UserUtils } from "@webpack/common";
import settings from "./settings";
import { ChannelDelete, ChannelType, GuildDelete, RelationshipRemove, RelationshipType } from "./types";
import { deleteGroup, deleteGuild, getGroup, getGuild, notify } from "./utils";
let manuallyRemovedFriend: string | undefined;
let manuallyRemovedGuild: string | undefined;
let manuallyRemovedGroup: string | undefined;
export const removeFriend = (id: string) => manuallyRemovedFriend = id;
export const removeGuild = (id: string) => manuallyRemovedGuild = id;
export const removeGroup = (id: string) => manuallyRemovedGroup = id;
export async function onRelationshipRemove({ relationship: { type, id } }: RelationshipRemove) {
if (manuallyRemovedFriend === id) {
manuallyRemovedFriend = undefined;
return;
}
const user = await UserUtils.fetchUser(id)
.catch(() => null);
if (!user) return;
switch (type) {
case RelationshipType.FRIEND:
if (settings.store.friends)
notify(`${user.tag} removed you as a friend.`, user.getAvatarURL(undefined, undefined, false));
break;
case RelationshipType.FRIEND_REQUEST:
if (settings.store.friendRequestCancels)
notify(`A friend request from ${user.tag} has been removed.`, user.getAvatarURL(undefined, undefined, false));
break;
}
}
export function onGuildDelete({ guild: { id, unavailable } }: GuildDelete) {
if (!settings.store.servers) return;
if (unavailable) return;
if (manuallyRemovedGuild === id) {
deleteGuild(id);
manuallyRemovedGuild = undefined;
return;
}
const guild = getGuild(id);
if (guild) {
deleteGuild(id);
notify(`You were removed from the server ${guild.name}.`, guild.iconURL);
}
}
export function onChannelDelete({ channel: { id, type } }: ChannelDelete) {
if (!settings.store.groups) return;
if (type !== ChannelType.GROUP_DM) return;
if (manuallyRemovedGroup === id) {
deleteGroup(id);
manuallyRemovedGroup = undefined;
return;
}
const group = getGroup(id);
if (group) {
deleteGroup(id);
notify(`You were removed from the group ${group.name}.`, group.iconURL);
}
}

View File

@ -0,0 +1,72 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { forEachEvent } from "./events";
import { removeFriend, removeGroup, removeGuild } from "./functions";
import settings from "./settings";
import { syncAndRunChecks } from "./utils";
export default definePlugin({
name: "RelationshipNotifier",
description: "Notifies you when a friend, group chat, or server removes you.",
authors: [Devs.nick],
settings,
patches: [
{
find: "removeRelationship:function(",
replacement: {
match: /(removeRelationship:function\((\i),\i,\i\){)/,
replace: "$1$self.removeFriend($2);"
}
},
{
find: "leaveGuild:function(",
replacement: {
match: /(leaveGuild:function\((\i)\){)/,
replace: "$1$self.removeGuild($2);"
}
},
{
find: "closePrivateChannel:function(",
replacement: {
match: /(closePrivateChannel:function\((\i)\){)/,
replace: "$1$self.removeGroup($2);"
}
}
],
async start() {
setTimeout(() => {
syncAndRunChecks();
}, 5000);
forEachEvent((ev, cb) => FluxDispatcher.subscribe(ev, cb));
},
stop() {
forEachEvent((ev, cb) => FluxDispatcher.unsubscribe(ev, cb));
},
removeFriend,
removeGroup,
removeGuild
});

View File

@ -0,0 +1,53 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/settings";
import { OptionType } from "@utils/types";
export default definePluginSettings({
notices: {
type: OptionType.BOOLEAN,
description: "Also show a notice at the top of your screen when removed (use this if you don't want to miss any notifications).",
default: false
},
offlineRemovals: {
type: OptionType.BOOLEAN,
description: "Notify you when starting discord if you were removed while offline.",
default: true
},
friends: {
type: OptionType.BOOLEAN,
description: "Notify when a friend removes you",
default: true
},
friendRequestCancels: {
type: OptionType.BOOLEAN,
description: "Notify when a friend request is cancelled",
default: true
},
servers: {
type: OptionType.BOOLEAN,
description: "Notify when removed from a server",
default: true
},
groups: {
type: OptionType.BOOLEAN,
description: "Notify when removed from a group chat",
default: true
}
});

View File

@ -0,0 +1,62 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Channel } from "discord-types/general";
export interface ChannelDelete {
type: "CHANNEL_DELETE";
channel: Channel;
}
export interface GuildDelete {
type: "GUILD_DELETE";
guild: {
id: string;
unavailable?: boolean;
};
}
export interface RelationshipRemove {
type: "RELATIONSHIP_REMOVE";
relationship: {
id: string;
nickname: string;
type: number;
};
}
export interface SimpleGroupChannel {
id: string;
name: string;
iconURL?: string;
}
export interface SimpleGuild {
id: string;
name: string;
iconURL?: string;
}
export const enum ChannelType {
GROUP_DM = 3,
}
export const enum RelationshipType {
FRIEND = 1,
FRIEND_REQUEST = 3,
}

View File

@ -0,0 +1,149 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DataStore, Notices } from "@api/index";
import { showNotification } from "@api/Notifications";
import { ChannelStore, GuildStore, RelationshipStore, UserUtils } from "@webpack/common";
import settings from "./settings";
import { ChannelType, RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types";
const guilds = new Map<string, SimpleGuild>();
const groups = new Map<string, SimpleGroupChannel>();
const friends = {
friends: [] as string[],
requests: [] as string[]
};
export async function syncAndRunChecks() {
const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([
"relationship-notifier-guilds",
"relationship-notifier-groups",
"relationship-notifier-friends"
]) as [Map<string, SimpleGuild> | undefined, Map<string, SimpleGroupChannel> | undefined, Record<"friends" | "requests", string[]> | undefined];
await Promise.all([syncGuilds(), syncGroups(), syncFriends()]);
if (settings.store.offlineRemovals) {
if (settings.store.groups && oldGroups?.size) {
for (const [id, group] of oldGroups) {
if (!groups.has(id))
notify(`You are no longer in the group ${group.name}.`, group.iconURL);
}
}
if (settings.store.servers && oldGuilds?.size) {
for (const [id, guild] of oldGuilds) {
if (!guilds.has(id))
notify(`You are no longer in the server ${guild.name}.`, guild.iconURL);
}
}
if (settings.store.friends && oldFriends?.friends.length) {
for (const id of oldFriends.friends) {
if (friends.friends.includes(id)) continue;
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(`You are no longer friends with ${user.tag}.`, user.getAvatarURL(undefined, undefined, false));
}
}
if (settings.store.friendRequestCancels && oldFriends?.requests?.length) {
for (const id of oldFriends.requests) {
if (friends.requests.includes(id)) continue;
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(`Friend request from ${user.tag} has been revoked.`, user.getAvatarURL(undefined, undefined, false));
}
}
}
}
export function notify(text: string, icon?: string) {
if (settings.store.notices)
Notices.showNotice(text, "OK", () => Notices.popNotice());
showNotification({
title: "Relationship Notifier",
body: text,
icon
});
}
export function getGuild(id: string) {
return guilds.get(id);
}
export function deleteGuild(id: string) {
guilds.delete(id);
syncGuilds();
}
export async function syncGuilds() {
for (const [id, { name, icon }] of Object.entries(GuildStore.getGuilds())) {
guilds.set(id, {
id,
name,
iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png`
});
}
await DataStore.set("relationship-notifier-guilds", guilds);
}
export function getGroup(id: string) {
return groups.get(id);
}
export function deleteGroup(id: string) {
groups.delete(id);
syncGroups();
}
export async function syncGroups() {
for (const { type, id, name, rawRecipients, icon } of ChannelStore.getSortedPrivateChannels()) {
if (type === ChannelType.GROUP_DM)
groups.set(id, {
id,
name: name || rawRecipients.map(r => r.username).join(", "),
iconURL: icon && `https://cdn.discordapp.com/channel-icons/${id}/${icon}.png`
});
}
await DataStore.set("relationship-notifier-groups", groups);
}
export async function syncFriends() {
friends.friends = [];
friends.requests = [];
const relationShips = RelationshipStore.getRelationships();
for (const id in relationShips) {
switch (relationShips[id]) {
case RelationshipType.FRIEND:
friends.friends.push(id);
break;
case RelationshipType.FRIEND_REQUEST:
friends.requests.push(id);
break;
}
}
await DataStore.set("relationship-notifier-friends", friends);
}

View File

@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { debounce } from "@utils/debounce";
import { classes, LazyComponent } from "@utils/misc";
import { classes, copyWithToast, LazyComponent } from "@utils/misc";
import { filters, find } from "@webpack";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
@ -74,6 +74,37 @@ function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
);
}
function CopyContextMenu({ name, path }: { name: string; path: string; }) {
const copyId = `spotify-copy-${name}`;
const openId = `spotify-open-${name}`;
return (
<Menu.ContextMenu
navId={`spotify-${name}-menu`}
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label={`Spotify ${name} Menu`}
>
<Menu.MenuItem
key={copyId}
id={copyId}
label={`Copy ${name} Link`}
action={() => copyWithToast("https://open.spotify.com" + path)}
/>
<Menu.MenuItem
key={openId}
id={openId}
label={`Open ${name} in Spotify`}
action={() => SpotifyStore.openExternal(path)}
/>
</Menu.ContextMenu>
);
}
function makeContextMenu(name: string, path: string) {
return (e: React.MouseEvent<HTMLElement, MouseEvent>) =>
ContextMenu.open(e, () => <CopyContextMenu name={name} path={path} />);
}
function Controls() {
const [isPlaying, shuffle, repeat] = useStateFromStores(
[SpotifyStore],
@ -263,6 +294,7 @@ function Info({ track }: { track: Track; }) {
onClick={track.id ? () => {
SpotifyStore.openExternal(`/track/${track.id}`);
} : void 0}
onContextMenu={track.id ? makeContextMenu("Song", `/track/${track.id}`) : void 0}
>
{track.name}
</Forms.FormText>
@ -277,6 +309,7 @@ function Info({ track }: { track: Track; }) {
href={`https://open.spotify.com/artist/${a.id}`}
style={{ fontSize: "inherit" }}
title={a.name}
onContextMenu={makeContextMenu("Artist", `/artist/${a.id}`)}
>
{a.name}
</Link>
@ -295,6 +328,7 @@ function Info({ track }: { track: Track; }) {
disabled={!track.album.id}
style={{ fontSize: "inherit" }}
title={track.album.name}
onContextMenu={makeContextMenu("Album", `/album/${track.album.id}`)}
>
{track.album.name}
</Link>

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/settings";
import IpcEvents from "@utils/IpcEvents";
import { proxyLazy } from "@utils/proxyLazy";
import { findByPropsLazy } from "@webpack";
@ -89,7 +90,11 @@ export const SpotifyStore = proxyLazy(() => {
public isSettingPosition = false;
public openExternal(path: string) {
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://open.spotify.com" + path);
const url = Settings.plugins.SpotifyControls.useSpotifyUris
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
: "https://open.spotify.com" + path;
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, url);
}
// Need to keep track of this manually

View File

@ -47,6 +47,11 @@ export default definePlugin({
default: false,
onChange: v => toggleHoverControls(v)
},
useSpotifyUris: {
type: OptionType.BOOLEAN,
description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms",
default: false
}
},
patches: [
{

View File

@ -5,6 +5,14 @@
--vc-spotify-green: #1db954; /* so cusotm themes can easily change it */
}
.theme-light #vc-spotify-player {
background: var(--bg-overlay-3, var(--background-secondary-alt));
}
.theme-dark #vc-spotify-player {
background: var(--bg-overlay-1, var(--background-secondary-alt));
}
.vc-spotify-button {
background: none;
color: var(--interactive-normal);

110
src/plugins/wikisearch.ts Normal file
View File

@ -0,0 +1,110 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "Wikisearch",
description: "Searches Wikipedia for your requested query. (/wikisearch)",
authors: [Devs.Samu],
dependencies: ["CommandsAPI"],
commands: [
{
name: "wikisearch",
description: "Searches Wikipedia for your request.",
inputType: ApplicationCommandInputType.BUILT_IN,
options: [
{
name: "search",
description: "Word to search for",
type: ApplicationCommandOptionType.STRING,
required: true
},
],
execute: async (_, ctx) => {
const word = findOption(_, "search", "");
if (!word) {
return sendBotMessage(ctx.channel.id, {
content: "No word was defined!"
});
}
const dataSearchParams = new URLSearchParams({
action: "query",
format: "json",
list: "search",
formatversion: "2",
origin: "*",
srsearch: word
});
const data = await fetch("https://en.wikipedia.org/w/api.php?" + dataSearchParams).then(response => response.json())
.catch(err => {
console.log(err);
sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" });
return null;
});
if (!data) return;
if (!data.query?.search?.length) {
console.log(data);
return sendBotMessage(ctx.channel.id, { content: "No results given" });
}
const altData = await fetch(`https://en.wikipedia.org/w/api.php?action=query&format=json&prop=info%7Cdescription%7Cimages%7Cimageinfo%7Cpageimages&list=&meta=&indexpageids=1&pageids=${data.query.search[0].pageid}&formatversion=2&origin=*`)
.then(res => res.json())
.then(data => data.query.pages[0])
.catch(err => {
console.log(err);
sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" });
return null;
});
if (!altData) return;
const thumbnailData = altData.thumbnail;
const thumbnail = thumbnailData && {
url: thumbnailData.source.replace(/(50px-)/ig, "1000px-"),
height: thumbnailData.height * 100,
width: thumbnailData.width * 100
};
sendBotMessage(ctx.channel.id, {
embeds: [
{
type: "rich",
title: data.query.search[0].title,
url: `https://wikipedia.org/w/index.php?curid=${data.query.search[0].pageid}`,
color: "0x8663BE",
description: data.query.search[0].snippet.replace(/(&nbsp;|<([^>]+)>)/ig, "").replace(/(&quot;)/ig, "\"") + "...",
image: thumbnail,
footer: {
text: "Powered by the Wikimedia API",
},
}
] as any
});
}
}
]
});

View File

@ -194,6 +194,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Captain",
id: 347366054806159360n
},
nick: {
name: "nick",
id: 347884694408265729n
},
whqwert: {
name: "whqwert",
id: 586239091520176128n

View File

@ -24,10 +24,12 @@ export let useState: typeof React.useState;
export let useEffect: typeof React.useEffect;
export let useMemo: typeof React.useMemo;
export let useRef: typeof React.useRef;
export let useReducer: typeof React.useReducer;
export let useCallback: typeof React.useCallback;
export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
waitFor("useState", m => {
React = m;
({ useEffect, useState, useMemo, useRef } = React);
({ useEffect, useState, useMemo, useRef, useReducer, useCallback } = React);
});