Merge branch 'dev' into feat/usercss

This commit is contained in:
Lewis Crichton 2023-09-15 19:40:07 +01:00 committed by GitHub
commit 06f2239b1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 320 additions and 24 deletions

View 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

View 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),"
}
]
}],
});

View 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;
}

View file

@ -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;"
}; };
}) })

View 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)

View file

@ -0,0 +1,5 @@
# FavoriteGifSearch
Adds a search bar to favorite gifs.
![Screenshot](https://i.imgur.com/Bcgb7PD.png)

View file

@ -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: [
{ {

View 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)

View file

@ -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"

View file

@ -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);

View file

@ -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;
} }

View file

@ -76,6 +76,8 @@ const enum NameFormat {
StatusName = "status-name", StatusName = "status-name",
ArtistFirst = "artist-first", ArtistFirst = "artist-first",
SongFirst = "song-first", SongFirst = "song-first",
ArtistOnly = "artist",
SongOnly = "song"
} }
const applicationId = "1108588077900898414"; const applicationId = "1108588077900898414";
@ -143,6 +145,14 @@ const settings = definePluginSettings({
{ {
label: "Use format 'song - artist'", label: "Use format 'song - artist'",
value: NameFormat.SongFirst value: NameFormat.SongFirst
},
{
label: "Use artist name only",
value: NameFormat.ArtistOnly
},
{
label: "Use song name only",
value: NameFormat.SongOnly
} }
], ],
}, },
@ -171,7 +181,7 @@ const settings = definePluginSettings({
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, Devs.blahajZip], authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
settingsAboutComponent: () => ( settingsAboutComponent: () => (
<> <>
@ -298,6 +308,10 @@ export default definePlugin({
return trackData.artist + " - " + trackData.name; return trackData.artist + " - " + trackData.name;
case NameFormat.SongFirst: case NameFormat.SongFirst:
return trackData.name + " - " + trackData.artist; return trackData.name + " - " + trackData.artist;
case NameFormat.ArtistOnly:
return trackData.artist;
case NameFormat.SongOnly:
return trackData.name;
default: default:
return settings.store.statusName; return settings.store.statusName;
} }

View file

@ -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 ' : '') +"
} }
] ]

View file

@ -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(),"
}, },
}, },
], ],

View file

@ -0,0 +1,5 @@
# PreviewMessage
Lets you preview your message before sending it.
![Example](https://i.imgur.com/etqbkzu.png)

View file

@ -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: [
{ {

View file

@ -0,0 +1,5 @@
# SearchReply
Adds a reply button to search results.
![Screenshot](https://i.imgur.com/SjIEHpw.png)

View file

@ -370,6 +370,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
blahajZip: { blahajZip: {
name: "blahaj.zip", name: "blahaj.zip",
id: 683954422241427471n, id: 683954422241427471n,
},
archeruwu: {
name: "archer_uwu",
id: 160068695383736320n
} }
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);

View file

@ -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 {