Compare commits
14 Commits
v1.4.7
...
feat/start
Author | SHA1 | Date | |
---|---|---|---|
|
9bc373d87a | ||
|
fb7e1c64fd | ||
|
1128e8f3ad | ||
|
a73d09a2f0 | ||
|
cf7c4d63b6 | ||
|
a95311ef2c | ||
|
dd23f9802c | ||
|
f23ddf4cae | ||
|
4222c7fd9f | ||
|
09f65b401e | ||
|
7364776715 | ||
|
3a5b70d410 | ||
|
e08d49edac | ||
|
17abbd3e3e |
5
src/plugins/dearrow/README.md
Normal file
5
src/plugins/dearrow/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Dearrow
|
||||||
|
|
||||||
|
Makes YouTube embed titles and thumbnails less sensationalist, powered by [Dearrow](https://dearrow.ajay.app/)
|
||||||
|
|
||||||
|
https://github.com/Vendicated/Vencord/assets/45497981/7bf81108-102d-47c5-8ba5-357db4db1283
|
161
src/plugins/dearrow/index.tsx
Normal file
161
src/plugins/dearrow/index.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Tooltip } from "@webpack/common";
|
||||||
|
import type { Component } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
embed: {
|
||||||
|
rawTitle: string;
|
||||||
|
provider?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
thumbnail: {
|
||||||
|
proxyURL: string;
|
||||||
|
};
|
||||||
|
video: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
dearrow: {
|
||||||
|
enabled: boolean;
|
||||||
|
oldTitle?: string;
|
||||||
|
oldThumb?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
||||||
|
|
||||||
|
async function embedDidMount(this: Component<Props>) {
|
||||||
|
try {
|
||||||
|
const { embed } = this.props;
|
||||||
|
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||||
|
|
||||||
|
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||||
|
if (!videoId) return;
|
||||||
|
|
||||||
|
const res = await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
const { titles, thumbnails } = await res.json();
|
||||||
|
|
||||||
|
const hasTitle = titles[0]?.votes >= 0;
|
||||||
|
const hasThumb = thumbnails[0]?.votes >= 0;
|
||||||
|
|
||||||
|
if (!hasTitle && !hasThumb) return;
|
||||||
|
|
||||||
|
embed.dearrow = {
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (titles[0]?.votes >= 0) {
|
||||||
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
|
embed.rawTitle = titles[0].title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnails[0]?.votes >= 0) {
|
||||||
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
|
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.forceUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
new Logger("Dearrow").error("Failed to dearrow embed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DearrowButton({ component }: { component: Component<Props>; }) {
|
||||||
|
const { embed } = component.props;
|
||||||
|
if (!embed?.dearrow) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={embed.dearrow.enabled ? "This embed has been dearrowed, click to restore" : "Click to dearrow"}>
|
||||||
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
<button
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
||||||
|
onClick={() => {
|
||||||
|
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
||||||
|
embed.dearrow.enabled = !enabled;
|
||||||
|
if (oldTitle) {
|
||||||
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
|
embed.rawTitle = oldTitle;
|
||||||
|
}
|
||||||
|
if (oldThumb) {
|
||||||
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
|
embed.thumbnail.proxyURL = oldThumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.forceUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Dearrow Icon, taken from https://dearrow.ajay.app/logo.svg (and optimised) */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
aria-label="Toggle Dearrow"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1213BD"
|
||||||
|
d="M36 18.302c0 4.981-2.46 9.198-5.655 12.462s-7.323 5.152-12.199 5.152s-9.764-1.112-12.959-4.376S0 23.283 0 18.302s2.574-9.38 5.769-12.644S13.271 0 18.146 0s9.394 2.178 12.589 5.442C33.931 8.706 36 13.322 36 18.302z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#88c9f9"
|
||||||
|
d="m 30.394282,18.410186 c 0,3.468849 -1.143025,6.865475 -3.416513,9.137917 -2.273489,2.272442 -5.670115,2.92874 -9.137918,2.92874 -3.467803,0 -6.373515,-1.147212 -8.6470033,-3.419654 -2.2734888,-2.272442 -3.5871299,-5.178154 -3.5871299,-8.647003 0,-3.46885 0.9420533,-6.746149 3.2144954,-9.0196379 2.2724418,-2.2734888 5.5507878,-3.9513905 9.0196378,-3.9513905 3.46885,0 6.492841,1.9322561 8.76633,4.204698 2.273489,2.2724424 3.788101,5.2974804 3.788101,8.7663304 z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#0a62a5"
|
||||||
|
d="m 23.95823,17.818306 c 0,3.153748 -2.644888,5.808102 -5.798635,5.808102 -3.153748,0 -5.599825,-2.654354 -5.599825,-5.808102 0,-3.153747 2.446077,-5.721714 5.599825,-5.721714 3.153747,0 5.798635,2.567967 5.798635,5.721714 z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "Dearrow",
|
||||||
|
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
embedDidMount,
|
||||||
|
renderButton(component: Component<Props>) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary noop>
|
||||||
|
<DearrowButton component={component} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
find: "this.renderInlineMediaEmbed",
|
||||||
|
replacement: [
|
||||||
|
// patch componentDidMount to replace embed thumbnail and title
|
||||||
|
{
|
||||||
|
match: /(\i).render=function.{0,50}\i\.embed/,
|
||||||
|
replace: "$1.componentDidMount=$self.embedDidMount,$&"
|
||||||
|
},
|
||||||
|
|
||||||
|
// add dearrow button
|
||||||
|
{
|
||||||
|
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
||||||
|
replace: "children:[$self.renderButton(this),"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
});
|
12
src/plugins/dearrow/styles.css
Normal file
12
src/plugins/dearrow/styles.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.vc-dearrow-toggle-off svg {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-dearrow-toggle-on, .vc-dearrow-toggle-off {
|
||||||
|
all: unset;
|
||||||
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
}
|
@ -212,15 +212,15 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canStreamHighQuality:function",
|
find: "canUseHighVideoUploadQuality:function",
|
||||||
predicate: () => settings.store.enableStreamQualityBypass,
|
predicate: () => settings.store.enableStreamQualityBypass,
|
||||||
replacement: [
|
replacement: [
|
||||||
"canUseHighVideoUploadQuality",
|
"canUseHighVideoUploadQuality",
|
||||||
"canStreamHighQuality",
|
// TODO: Remove the last two when they get removed from stable
|
||||||
"canStreamMidQuality"
|
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
|
||||||
].map(func => {
|
].map(func => {
|
||||||
return {
|
return {
|
||||||
match: new RegExp(`${func}:function\\(\\i\\){`),
|
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
|
||||||
replace: "$&return true;"
|
replace: "$&return true;"
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
6
src/plugins/favEmojiFirst/README.md
Normal file
6
src/plugins/favEmojiFirst/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# FavoriteEmojiFirst
|
||||||
|
|
||||||
|
Puts your favorite emoji first in the emoji autocomplete.
|
||||||
|
|
||||||
|
![FavEmojis](https://i.imgur.com/mEFCoZG.png)
|
||||||
|
![Example](https://i.imgur.com/wY3Tc43.png)
|
5
src/plugins/favGifSearch/README.md
Normal file
5
src/plugins/favGifSearch/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# FavoriteGifSearch
|
||||||
|
|
||||||
|
Adds a search bar to favorite gifs.
|
||||||
|
|
||||||
|
![Screenshot](https://i.imgur.com/Bcgb7PD.png)
|
@ -87,7 +87,7 @@ export const settings = definePluginSettings({
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FavoriteGifSearch",
|
name: "FavoriteGifSearch",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria],
|
||||||
description: "Adds a search bar for favorite gifs",
|
description: "Adds a search bar to favorite gifs.",
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
6
src/plugins/imageZoom/README.md
Normal file
6
src/plugins/imageZoom/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# ImageZoom
|
||||||
|
|
||||||
|
Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size
|
||||||
|
|
||||||
|
![Example](https://i.imgur.com/VJdo4aq.png)
|
||||||
|
![ContextMenu](https://i.imgur.com/0oaRM2s.png)
|
@ -99,6 +99,15 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
|||||||
ContextMenu.close();
|
ContextMenu.close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id="vc-nearest-neighbour"
|
||||||
|
label="Nearset Neighbour"
|
||||||
|
checked={settings.store.nearestNeighbour}
|
||||||
|
action={() => {
|
||||||
|
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
|
||||||
|
ContextMenu.close();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Menu.MenuControlItem
|
<Menu.MenuControlItem
|
||||||
id="vc-zoom"
|
id="vc-zoom"
|
||||||
label="Zoom"
|
label="Zoom"
|
||||||
|
@ -28,7 +28,7 @@ import { Button, Forms, React, TextInput } from "@webpack/common";
|
|||||||
import { decrypt } from "../index";
|
import { decrypt } from "../index";
|
||||||
|
|
||||||
export function DecModal(props: any) {
|
export function DecModal(props: any) {
|
||||||
const secret: string = props?.message?.content;
|
const encryptedMessage: string = props?.message?.content;
|
||||||
const [password, setPassword] = React.useState("password");
|
const [password, setPassword] = React.useState("password");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,9 +38,9 @@ export function DecModal(props: any) {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Message with Encryption</Forms.FormTitle>
|
||||||
<TextInput defaultValue={secret} disabled={true}></TextInput>
|
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
|
||||||
<Forms.FormTitle tag="h5">Password</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{ marginBottom: "20px" }}
|
style={{ marginBottom: "20px" }}
|
||||||
onChange={setPassword}
|
onChange={setPassword}
|
||||||
@ -51,7 +51,7 @@ export function DecModal(props: any) {
|
|||||||
<Button
|
<Button
|
||||||
color={Button.Colors.GREEN}
|
color={Button.Colors.GREEN}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const toSend = decrypt(secret, password, true);
|
const toSend = decrypt(encryptedMessage, password, true);
|
||||||
if (!toSend || !props?.message) return;
|
if (!toSend || !props?.message) return;
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
||||||
|
@ -225,8 +225,8 @@ export function encrypt(secret: string, password: string, cover: string): string
|
|||||||
return steggo.hide(secret + "\u200b", password, cover);
|
return steggo.hide(secret + "\u200b", password, cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
|
export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string {
|
||||||
const decrypted = steggo.reveal(secret, password);
|
const decrypted = steggo.reveal(encrypted, password);
|
||||||
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,14 @@ const enum ActivityFlag {
|
|||||||
INSTANCE = 1 << 0,
|
INSTANCE = 1 << 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum NameFormat {
|
||||||
|
StatusName = "status-name",
|
||||||
|
ArtistFirst = "artist-first",
|
||||||
|
SongFirst = "song-first",
|
||||||
|
ArtistOnly = "artist",
|
||||||
|
SongOnly = "song"
|
||||||
|
}
|
||||||
|
|
||||||
const applicationId = "1108588077900898414";
|
const applicationId = "1108588077900898414";
|
||||||
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
||||||
|
|
||||||
@ -117,10 +125,37 @@ const settings = definePluginSettings({
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
statusName: {
|
statusName: {
|
||||||
description: "text shown in status",
|
description: "custom status text",
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
default: "some music",
|
default: "some music",
|
||||||
},
|
},
|
||||||
|
nameFormat: {
|
||||||
|
description: "Show name of song and artist in status name",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Use custom status name",
|
||||||
|
value: NameFormat.StatusName,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use format 'artist - song'",
|
||||||
|
value: NameFormat.ArtistFirst
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use format 'song - artist'",
|
||||||
|
value: NameFormat.SongFirst
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use artist name only",
|
||||||
|
value: NameFormat.ArtistOnly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use song name only",
|
||||||
|
value: NameFormat.SongOnly
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
useListeningStatus: {
|
useListeningStatus: {
|
||||||
description: 'show "Listening to" status instead of "Playing"',
|
description: 'show "Listening to" status instead of "Playing"',
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
@ -140,13 +175,13 @@ const settings = definePluginSettings({
|
|||||||
value: "placeholder"
|
value: "placeholder"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "LastFMRichPresence",
|
name: "LastFMRichPresence",
|
||||||
description: "Little plugin for Last.fm rich presence",
|
description: "Little plugin for Last.fm rich presence",
|
||||||
authors: [Devs.dzshn, Devs.RuiNtD],
|
authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
@ -267,9 +302,24 @@ export default definePlugin({
|
|||||||
url: `https://www.last.fm/user/${settings.store.username}`,
|
url: `https://www.last.fm/user/${settings.store.username}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusName = (() => {
|
||||||
|
switch (settings.store.nameFormat) {
|
||||||
|
case NameFormat.ArtistFirst:
|
||||||
|
return trackData.artist + " - " + trackData.name;
|
||||||
|
case NameFormat.SongFirst:
|
||||||
|
return trackData.name + " - " + trackData.artist;
|
||||||
|
case NameFormat.ArtistOnly:
|
||||||
|
return trackData.artist;
|
||||||
|
case NameFormat.SongOnly:
|
||||||
|
return trackData.name;
|
||||||
|
default:
|
||||||
|
return settings.store.statusName;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
application_id: applicationId,
|
application_id: applicationId,
|
||||||
name: settings.store.statusName,
|
name: statusName,
|
||||||
|
|
||||||
details: trackData.name,
|
details: trackData.name,
|
||||||
state: trackData.artist,
|
state: trackData.artist,
|
||||||
|
@ -326,14 +326,14 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
// Attachment renderer
|
// Attachment renderer
|
||||||
// Module 96063
|
// Module 96063
|
||||||
find: "[\"className\",\"attachment\",\"inlineMedia\"",
|
find: "().removeAttachmentHoverButton",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /((\w)\.className,\w=\2\.attachment),/,
|
match: /((\w)\.className,\w=\2\.attachment),/,
|
||||||
replace: "$1,deleted=$2.attachment?.deleted,"
|
replace: "$1,deleted=$2.attachment?.deleted,"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /\["className","attachment","inlineMedia".+?className:/,
|
match: /\["className","attachment".+?className:/,
|
||||||
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -29,8 +29,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".onRemoveAttachment,",
|
find: ".onRemoveAttachment,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\.nonMediaAttachment.{0,10}children:\[(\i),/,
|
match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/,
|
||||||
replace: "$&$1&&$self.renderPiPButton(),"
|
replace: "$&$1&&$2&&$self.renderPiPButton(),"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
5
src/plugins/previewMessage/README.md
Normal file
5
src/plugins/previewMessage/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# PreviewMessage
|
||||||
|
|
||||||
|
Lets you preview your message before sending it.
|
||||||
|
|
||||||
|
![Example](https://i.imgur.com/etqbkzu.png)
|
@ -16,37 +16,96 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { sendBotMessage } from "@api/Commands";
|
import { generateId, sendBotMessage } from "@api/Commands";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||||
|
import { MessageAttachment } from "discord-types/general";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: {
|
type: {
|
||||||
analyticsName: string;
|
analyticsName: string;
|
||||||
|
isEmpty: boolean;
|
||||||
|
attachments: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UploadStore = findByPropsLazy("getUploads");
|
||||||
|
|
||||||
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
||||||
|
|
||||||
|
|
||||||
|
const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
|
||||||
|
new Promise(res => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () =>
|
||||||
|
res({ width: img.width, height: img.height });
|
||||||
|
|
||||||
|
img.onerror = () =>
|
||||||
|
res(null);
|
||||||
|
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const getAttachments = async (channelId: string) =>
|
||||||
|
await Promise.all(
|
||||||
|
UploadStore.getUploads(channelId, DraftType.ChannelMessage)
|
||||||
|
.map(async (upload: any) => {
|
||||||
|
const { isImage, filename, spoiler, item: { file } } = upload;
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
const attachment: MessageAttachment = {
|
||||||
|
id: generateId(),
|
||||||
|
filename: spoiler ? "SPOILER_" + filename : filename,
|
||||||
|
// weird eh? if i give it the normal content type the preview doenst work
|
||||||
|
content_type: undefined,
|
||||||
|
size: await upload.getSize(),
|
||||||
|
spoiler,
|
||||||
|
// discord adds query params to the url, so we need to add a hash to prevent that
|
||||||
|
url: url + "#",
|
||||||
|
proxy_url: url + "#",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
const box = await getImageBox(url);
|
||||||
|
if (!box) return attachment;
|
||||||
|
|
||||||
|
attachment.width = box.width;
|
||||||
|
attachment.height = box.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
export function PreviewButton(chatBoxProps: Props) {
|
export function PreviewButton(chatBoxProps: Props) {
|
||||||
|
const { isEmpty, attachments } = chatBoxProps.type;
|
||||||
|
|
||||||
const channelId = SelectedChannelStore.getChannelId();
|
const channelId = SelectedChannelStore.getChannelId();
|
||||||
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
|
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
|
||||||
|
|
||||||
if (chatBoxProps.type.analyticsName !== "normal") return null;
|
if (chatBoxProps.type.analyticsName !== "normal") return null;
|
||||||
if (!draft) return null;
|
|
||||||
|
const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0;
|
||||||
|
const hasContent = !isEmpty && draft?.length > 0;
|
||||||
|
|
||||||
|
if (!hasContent && !hasAttachments) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip text="Preview Message">
|
<Tooltip text="Preview Message">
|
||||||
{tooltipProps => (
|
{tooltipProps => (
|
||||||
<Button
|
<Button
|
||||||
{...tooltipProps}
|
{...tooltipProps}
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
sendBotMessage(
|
sendBotMessage(
|
||||||
channelId,
|
channelId,
|
||||||
{
|
{
|
||||||
content: getDraft(channelId),
|
content: getDraft(channelId),
|
||||||
author: UserStore.getCurrentUser()
|
author: UserStore.getCurrentUser(),
|
||||||
|
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
size=""
|
size=""
|
||||||
@ -66,7 +125,7 @@ export function PreviewButton(chatBoxProps: Props) {
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PreviewMessage",
|
name: "PreviewMessage",
|
||||||
description: "Lets you preview your message before sending it",
|
description: "Lets you preview your message before sending it.",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
5
src/plugins/searchReply/README.md
Normal file
5
src/plugins/searchReply/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# SearchReply
|
||||||
|
|
||||||
|
Adds a reply button to search results.
|
||||||
|
|
||||||
|
![Screenshot](https://i.imgur.com/SjIEHpw.png)
|
23
src/plugins/startupTimings/StartupTimingPage.css
Normal file
23
src/plugins/startupTimings/StartupTimingPage.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.vc-startuptimings-server-trace {
|
||||||
|
color: var(--header-primary);
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-grid {
|
||||||
|
color: var(--header-primary);
|
||||||
|
display: grid;
|
||||||
|
gap: 2px 10px;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-4-cols {
|
||||||
|
grid-template-columns: repeat(3, auto) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-3-cols {
|
||||||
|
grid-template-columns: repeat(2, auto) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-2-cols {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
@ -16,11 +16,35 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./StartupTimingPage.css";
|
||||||
|
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Forms, React } from "@webpack/common";
|
import { Forms, React } from "@webpack/common";
|
||||||
|
|
||||||
|
|
||||||
|
export const cl = classNameFactory("vc-startuptimings-");
|
||||||
|
|
||||||
|
interface ITTITrackerEvent {
|
||||||
|
emoji: string;
|
||||||
|
name: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
hasData(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITTITracker {
|
||||||
|
serializeTTITracker(): Record<string, string | number | boolean | null | undefined>;
|
||||||
|
[event: string]: ITTITrackerEvent | string | boolean | null | any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Time-To-Interactive Tracker */
|
||||||
|
const TTITracker: ITTITracker = findByPropsLazy("serializeTTITracker");
|
||||||
|
|
||||||
interface AppStartPerformance {
|
interface AppStartPerformance {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
logs: Log[];
|
logs: Log[];
|
||||||
@ -91,11 +115,11 @@ function TimingSection({ title, logs, traceEnd }: TimingSectionProps) {
|
|||||||
<Forms.FormSection title={title} tag="h1">
|
<Forms.FormSection title={title} tag="h1">
|
||||||
<code>
|
<code>
|
||||||
{traceEnd && (
|
{traceEnd && (
|
||||||
<div style={{ color: "var(--header-primary)", marginBottom: 5, userSelect: "text" }}>
|
<div className={cl("server-trace")} style={{ marginBottom: 5 }}>
|
||||||
Trace ended at: {(new Date(traceEnd)).toTimeString()}
|
Trace ended at: {(new Date(traceEnd)).toTimeString()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ color: "var(--header-primary)", display: "grid", gridTemplateColumns: "repeat(3, auto) 1fr", gap: "2px 10px", userSelect: "text" }}>
|
<div className={classes(cl("grid"), cl("4-cols"))}>
|
||||||
<span>Start</span>
|
<span>Start</span>
|
||||||
<span>Interval</span>
|
<span>Interval</span>
|
||||||
<span>Delta</span>
|
<span>Delta</span>
|
||||||
@ -119,13 +143,66 @@ function ServerTrace({ trace }: ServerTraceProps) {
|
|||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Server Trace" tag="h2">
|
<Forms.FormSection title="Server Trace" tag="h2">
|
||||||
<code>
|
<code>
|
||||||
<Flex flexDirection="column" style={{ color: "var(--header-primary)", gap: 5, userSelect: "text" }}>
|
<Flex flexDirection="column" className={cl("server-trace")} style={{ gap: 5 }}>
|
||||||
{lines.map(line => (
|
{lines.map(line => (
|
||||||
<span>{line}</span>
|
<span>{line}</span>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</code>
|
</code>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TTIAnalytics() {
|
||||||
|
const analytics = TTITracker.serializeTTITracker();
|
||||||
|
const filteredAnalytics = Object.entries(analytics).filter(([key, value]) => value != null && !/_start$|_end$/.test(key));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Forms.FormSection title="TTI Analytics" tag="h2">
|
||||||
|
<code>
|
||||||
|
<div className={classes(cl("grid"), cl("2-cols"))}>
|
||||||
|
{filteredAnalytics.map(([key, value]) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<span><pre>{key}</pre></span>
|
||||||
|
<span><pre>{`${value}`}</pre></span>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</code>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TTITimingsProps {
|
||||||
|
records: [string, ITTITrackerEvent][];
|
||||||
|
title: string;
|
||||||
|
type: "registered" | "unregistered";
|
||||||
|
}
|
||||||
|
|
||||||
|
function TTITimings({ records, title, type }: TTITimingsProps) {
|
||||||
|
const isRegistered = type === "registered";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Forms.FormSection title={title} tag="h2">
|
||||||
|
<code>
|
||||||
|
<div className={classes(cl("grid"), cl(isRegistered ? "3-cols" : "2-cols"))}>
|
||||||
|
{isRegistered && <span>Duration</span>}
|
||||||
|
<span>Key</span>
|
||||||
|
<span style={{ marginBottom: 5 }}>Event</span>
|
||||||
|
{records.map(([key, event]) => (
|
||||||
|
<React.Fragment key={key}>
|
||||||
|
{isRegistered && <span><pre>{event.end - event.start}ms</pre></span>}
|
||||||
|
<span><pre>{key}</pre></span>
|
||||||
|
<span><pre>{event.emoji} {event.name}</pre></span>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</code>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +211,12 @@ function StartupTimingPage() {
|
|||||||
|
|
||||||
const serverTrace = AppStartPerformance.logGroups.find(g => g.serverTrace)?.serverTrace;
|
const serverTrace = AppStartPerformance.logGroups.find(g => g.serverTrace)?.serverTrace;
|
||||||
|
|
||||||
|
const registeredTTITimings: [string, ITTITrackerEvent][] = (Object.entries(TTITracker))
|
||||||
|
.filter(([, value]) => value?.hasData?.());
|
||||||
|
|
||||||
|
const unregisteredTTITimings: [string, ITTITrackerEvent][] = (Object.entries(TTITracker))
|
||||||
|
.filter(([, value]) => value?.hasData && !value.hasData());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TimingSection
|
<TimingSection
|
||||||
@ -141,9 +224,26 @@ function StartupTimingPage() {
|
|||||||
logs={AppStartPerformance.logs}
|
logs={AppStartPerformance.logs}
|
||||||
traceEnd={AppStartPerformance.endTime_}
|
traceEnd={AppStartPerformance.endTime_}
|
||||||
/>
|
/>
|
||||||
{/* Lazy Divider */}
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
<div style={{ marginTop: 5 }}> </div>
|
|
||||||
{serverTrace && <ServerTrace trace={serverTrace} />}
|
{serverTrace && <ServerTrace trace={serverTrace} />}
|
||||||
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
|
|
||||||
|
<TTIAnalytics />
|
||||||
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
|
|
||||||
|
<TTITimings
|
||||||
|
title="Registered TTI Timings"
|
||||||
|
records={registeredTTITimings}
|
||||||
|
type="registered"
|
||||||
|
/>
|
||||||
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
|
|
||||||
|
<TTITimings
|
||||||
|
title="Unregistered TTI Timings"
|
||||||
|
records={unregisteredTTITimings}
|
||||||
|
type="unregistered"
|
||||||
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -366,6 +366,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||||||
RuukuLada: {
|
RuukuLada: {
|
||||||
name: "RuukuLada",
|
name: "RuukuLada",
|
||||||
id: 119705748346241027n,
|
id: 119705748346241027n,
|
||||||
|
},
|
||||||
|
blahajZip: {
|
||||||
|
name: "blahaj.zip",
|
||||||
|
id: 683954422241427471n,
|
||||||
|
},
|
||||||
|
archeruwu: {
|
||||||
|
name: "archer_uwu",
|
||||||
|
id: 160068695383736320n
|
||||||
}
|
}
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
|
9
src/webpack/common/types/menu.d.ts
vendored
9
src/webpack/common/types/menu.d.ts
vendored
@ -65,8 +65,13 @@ export interface Menu {
|
|||||||
id: string;
|
id: string;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
}>;
|
}>;
|
||||||
// TODO: Type me
|
MenuSliderControl: RC<{
|
||||||
MenuSliderControl: RC<any>;
|
minValue: number,
|
||||||
|
maxValue: number,
|
||||||
|
value: number,
|
||||||
|
onChange(value: number): void,
|
||||||
|
renderValue?(value: number): string,
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenuApi {
|
export interface ContextMenuApi {
|
||||||
|
Reference in New Issue
Block a user