mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-01-10 18:06:22 +00:00
Merge branch 'Vendicated:main' into main
This commit is contained in:
commit
e157097c7e
17 changed files with 228 additions and 115 deletions
|
@ -35,11 +35,11 @@ const ETAG_FILE = join(FILE_DIR, "etag.txt");
|
|||
function getFilename() {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "VencordInstaller.exe";
|
||||
return "VencordInstallerCli.exe";
|
||||
case "darwin":
|
||||
return "VencordInstaller.MacOS.zip";
|
||||
case "linux":
|
||||
return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
|
||||
return "VencordInstallerCli-linux";
|
||||
default:
|
||||
throw new Error("Unsupported platform: " + process.platform);
|
||||
}
|
||||
|
|
|
@ -83,10 +83,10 @@ function VencordSettings() {
|
|||
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||
note: "Requires a full restart"
|
||||
}),
|
||||
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: "Enable window transparency",
|
||||
note: "Requires a full restart"
|
||||
title: "Enable window transparency.",
|
||||
note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart"
|
||||
},
|
||||
!IS_WEB && isWindows && {
|
||||
key: "winCtrlQ",
|
||||
|
|
|
@ -79,8 +79,7 @@ if (!IS_VANILLA) {
|
|||
delete options.frame;
|
||||
}
|
||||
|
||||
// This causes electron to freeze / white screen for some people
|
||||
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
|
||||
if (settings.transparent) {
|
||||
options.transparent = true;
|
||||
options.backgroundColor = "#00000000";
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ export default definePlugin({
|
|||
{
|
||||
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
|
||||
replacement: {
|
||||
match: /(?<=children:\[)(?=.{10,80}tooltip:\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/,
|
||||
match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/,
|
||||
replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null,"
|
||||
},
|
||||
},
|
||||
|
|
5
src/plugins/fixYoutubeEmbeds.desktop/README.md
Normal file
5
src/plugins/fixYoutubeEmbeds.desktop/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# FixYoutubeEmbeds
|
||||
|
||||
Bypasses youtube videos being blocked from display on Discord (for example by UMG)
|
||||
|
||||
![](https://github.com/Vendicated/Vencord/assets/45497981/7a5fdcaa-217c-4c63-acae-f0d6af2f79be)
|
14
src/plugins/fixYoutubeEmbeds.desktop/index.ts
Normal file
14
src/plugins/fixYoutubeEmbeds.desktop/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "FixYoutubeEmbeds",
|
||||
description: "Bypasses youtube videos being blocked from display on Discord (for example by UMG)",
|
||||
authors: [Devs.coolelectronics]
|
||||
});
|
26
src/plugins/fixYoutubeEmbeds.desktop/native.ts
Normal file
26
src/plugins/fixYoutubeEmbeds.desktop/native.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { getSettings } from "main/ipcMain";
|
||||
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://www.youtube.com/")) {
|
||||
const settings = getSettings().plugins?.FixYoutubeEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
|
||||
frame.executeJavaScript(`
|
||||
new MutationObserver(() => {
|
||||
let err = document.querySelector(".ytp-error-content-wrap-subreason span")?.textContent;
|
||||
if (err && err.includes("blocked it from display")) window.location.reload()
|
||||
}).observe(document.body, { childList: true, subtree:true });
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -61,14 +61,17 @@ export function authorize(callback?: any) {
|
|||
const res = await fetch(url, {
|
||||
headers: new Headers({ Accept: "application/json" })
|
||||
});
|
||||
const { token, success } = await res.json();
|
||||
if (success) {
|
||||
updateAuth({ token });
|
||||
showToast("Successfully logged in!", Toasts.Type.SUCCESS);
|
||||
callback?.();
|
||||
} else if (res.status === 1) {
|
||||
showToast("An Error occurred while logging in.", Toasts.Type.FAILURE);
|
||||
|
||||
if (!res.ok) {
|
||||
const { message } = await res.json();
|
||||
showToast(message || "An error occured while authorizing", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
const { token } = await res.json();
|
||||
updateAuth({ token });
|
||||
showToast("Successfully logged in!", Toasts.Type.SUCCESS);
|
||||
callback?.();
|
||||
} catch (e) {
|
||||
new Logger("ReviewDB").error("Failed to authorize", e);
|
||||
}
|
||||
|
|
|
@ -20,13 +20,13 @@ import { openUserProfile } from "@utils/discord";
|
|||
import { classes } from "@utils/misc";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { filters, findBulk } from "@webpack";
|
||||
import { Alerts, moment, Parser, showToast, Timestamp } from "@webpack/common";
|
||||
import { Alerts, moment, Parser, Timestamp, useState } from "@webpack/common";
|
||||
|
||||
import { Auth, getToken } from "../auth";
|
||||
import { Review, ReviewType } from "../entities";
|
||||
import { blockUser, deleteReview, reportReview, unblockUser } from "../reviewDbApi";
|
||||
import { settings } from "../settings";
|
||||
import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl } from "../utils";
|
||||
import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl, showToast } from "../utils";
|
||||
import { openBlockModal } from "./BlockedUserModal";
|
||||
import { BlockButton, DeleteButton, ReportButton } from "./MessageButton";
|
||||
import ReviewBadge from "./ReviewBadge";
|
||||
|
@ -51,6 +51,8 @@ export default LazyComponent(() => {
|
|||
const dateFormat = new Intl.DateTimeFormat();
|
||||
|
||||
return function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
function openModal() {
|
||||
openUserProfile(review.sender.discordID);
|
||||
}
|
||||
|
@ -66,10 +68,9 @@ export default LazyComponent(() => {
|
|||
return showToast("You must be logged in to delete reviews.");
|
||||
} else {
|
||||
deleteReview(review.id).then(res => {
|
||||
if (res.success) {
|
||||
if (res) {
|
||||
refetch();
|
||||
}
|
||||
showToast(res.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -116,11 +117,11 @@ export default LazyComponent(() => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, cl("review"))} style={
|
||||
<div className={classes(cl("review"), cozyMessage, wrapper, message, groupStart, cozy)} style={
|
||||
{
|
||||
marginLeft: "0px",
|
||||
paddingLeft: "52px", // wth is this
|
||||
paddingRight: "16px"
|
||||
// nobody knows anymore
|
||||
}
|
||||
}>
|
||||
|
||||
|
@ -168,7 +169,9 @@ export default LazyComponent(() => {
|
|||
}
|
||||
|
||||
<div className={cl("review-comment")}>
|
||||
{Parser.parseGuildEventDescription(review.comment)}
|
||||
{(review.comment.length > 200 && !showAll)
|
||||
? [Parser.parseGuildEventDescription(review.comment.substring(0, 200)), "...", <br />, (<a onClick={() => setShowAll(true)}>Read more</a>)]
|
||||
: Parser.parseGuildEventDescription(review.comment)}
|
||||
</div>
|
||||
|
||||
{review.id !== 0 && (
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
|
||||
import { find, findByPropsLazy } from "@webpack";
|
||||
import { Forms, React, RelationshipStore, showToast, useRef, UserStore } from "@webpack/common";
|
||||
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
|
||||
|
||||
import { Auth, authorize } from "../auth";
|
||||
import { Review } from "../entities";
|
||||
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
|
||||
import { settings } from "../settings";
|
||||
import { cl } from "../utils";
|
||||
import { cl, showToast } from "../utils";
|
||||
import ReviewComponent from "./ReviewComponent";
|
||||
|
||||
|
||||
|
@ -168,7 +168,7 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: {
|
|||
comment: res.value,
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
if (response) {
|
||||
refetch();
|
||||
|
||||
const slateEditor = editorRef.current.ref.current.getSlateEditor();
|
||||
|
@ -180,8 +180,6 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: {
|
|||
focus: Editor.end(slateEditor, []),
|
||||
}
|
||||
});
|
||||
} else if (response?.message) {
|
||||
showToast(response.message);
|
||||
}
|
||||
|
||||
// even tho we need to return this, it doesnt do anything
|
||||
|
|
|
@ -25,7 +25,7 @@ import { OpenExternalIcon } from "@components/Icons";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Alerts, Menu, Parser, showToast, useState } from "@webpack/common";
|
||||
import { Alerts, Menu, Parser, useState } from "@webpack/common";
|
||||
import { Guild, User } from "discord-types/general";
|
||||
|
||||
import { Auth, initAuth, updateAuth } from "./auth";
|
||||
|
@ -34,6 +34,7 @@ import ReviewsView from "./components/ReviewsView";
|
|||
import { NotificationType } from "./entities";
|
||||
import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
|
||||
import { settings } from "./settings";
|
||||
import { showToast } from "./utils";
|
||||
|
||||
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
|
||||
children.push(
|
||||
|
|
|
@ -16,18 +16,18 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { showToast, Toasts } from "@webpack/common";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { Auth, authorize, getToken, updateAuth } from "./auth";
|
||||
import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities";
|
||||
import { settings } from "./settings";
|
||||
import { showToast } from "./utils";
|
||||
|
||||
const API_URL = "https://manti.vendicated.dev/api/reviewdb";
|
||||
|
||||
export const REVIEWS_PER_PAGE = 50;
|
||||
|
||||
export interface Response {
|
||||
success: boolean,
|
||||
message: string;
|
||||
reviews: Review[];
|
||||
updated: boolean;
|
||||
|
@ -37,6 +37,16 @@ export interface Response {
|
|||
|
||||
const WarningFlag = 0b00000010;
|
||||
|
||||
async function rdbRequest(path: string, options: RequestInit = {}) {
|
||||
return fetch(API_URL + path, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: await getToken() || "",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function getReviews(id: string, offset = 0): Promise<Response> {
|
||||
let flags = 0;
|
||||
if (!settings.store.showWarning) flags |= WarningFlag;
|
||||
|
@ -47,10 +57,9 @@ export async function getReviews(id: string, offset = 0): Promise<Response> {
|
|||
});
|
||||
const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`);
|
||||
|
||||
const res = (req.status === 200)
|
||||
const res = (req.ok)
|
||||
? await req.json() as Response
|
||||
: {
|
||||
success: false,
|
||||
message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.",
|
||||
reviews: [],
|
||||
updated: false,
|
||||
|
@ -58,7 +67,7 @@ export async function getReviews(id: string, offset = 0): Promise<Response> {
|
|||
reviewCount: 0
|
||||
};
|
||||
|
||||
if (!res.success) {
|
||||
if (!req.ok) {
|
||||
showToast(res.message, Toasts.Type.FAILURE);
|
||||
return {
|
||||
...res,
|
||||
|
@ -85,44 +94,46 @@ export async function getReviews(id: string, offset = 0): Promise<Response> {
|
|||
}
|
||||
|
||||
export async function addReview(review: any): Promise<Response | null> {
|
||||
review.token = await getToken();
|
||||
|
||||
if (!review.token) {
|
||||
const token = await getToken();
|
||||
if (!token) {
|
||||
showToast("Please authorize to add a review.");
|
||||
authorize();
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetch(API_URL + `/users/${review.userid}/reviews`, {
|
||||
return await rdbRequest(`/users/${review.userid}/reviews`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(review),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
showToast(res.message);
|
||||
return res ?? null;
|
||||
});
|
||||
}).then(async r => {
|
||||
const data = await r.json() as Response;
|
||||
showToast(data.message);
|
||||
return r.ok ? data : null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteReview(id: number): Promise<Response> {
|
||||
return fetch(API_URL + `/users/${id}/reviews`, {
|
||||
export async function deleteReview(id: number): Promise<Response | null> {
|
||||
return await rdbRequest(`/users/${id}/reviews`, {
|
||||
method: "DELETE",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
token: await getToken(),
|
||||
reviewid: id
|
||||
})
|
||||
}).then(r => r.json());
|
||||
}).then(async r => {
|
||||
const data = await r.json() as Response;
|
||||
showToast(data.message);
|
||||
return r.ok ? data : null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function reportReview(id: number) {
|
||||
const res = await fetch(API_URL + "/reports", {
|
||||
const res = await rdbRequest("/reports", {
|
||||
method: "PUT",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
|
@ -130,7 +141,6 @@ export async function reportReview(id: number) {
|
|||
}),
|
||||
body: JSON.stringify({
|
||||
reviewid: id,
|
||||
token: await getToken()
|
||||
})
|
||||
}).then(r => r.json()) as Response;
|
||||
|
||||
|
@ -138,12 +148,11 @@ export async function reportReview(id: number) {
|
|||
}
|
||||
|
||||
async function patchBlock(action: "block" | "unblock", userId: string) {
|
||||
const res = await fetch(API_URL + "/blocks", {
|
||||
const res = await rdbRequest("/blocks", {
|
||||
method: "PATCH",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: await getToken() || ""
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: action,
|
||||
|
@ -169,11 +178,10 @@ export const blockUser = (userId: string) => patchBlock("block", userId);
|
|||
export const unblockUser = (userId: string) => patchBlock("unblock", userId);
|
||||
|
||||
export async function fetchBlocks(): Promise<ReviewDBUser[]> {
|
||||
const res = await fetch(API_URL + "/blocks", {
|
||||
const res = await rdbRequest("/blocks", {
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
Authorization: await getToken() || ""
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -182,17 +190,13 @@ export async function fetchBlocks(): Promise<ReviewDBUser[]> {
|
|||
}
|
||||
|
||||
export function getCurrentUserInfo(token: string): Promise<ReviewDBCurrentUser> {
|
||||
return fetch(API_URL + "/users", {
|
||||
body: JSON.stringify({ token }),
|
||||
return rdbRequest("/users", {
|
||||
method: "POST",
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
export async function readNotification(id: number) {
|
||||
return fetch(API_URL + `/notifications?id=${id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Authorization": await getToken() || "",
|
||||
},
|
||||
return rdbRequest(`/notifications?id=${id}`, {
|
||||
method: "PATCH"
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,13 +22,14 @@ import { Button } from "@webpack/common";
|
|||
|
||||
import { authorize, getToken } from "./auth";
|
||||
import { openBlockModal } from "./components/BlockedUserModal";
|
||||
import { cl } from "./utils";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
authorize: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Authorize with ReviewDB",
|
||||
component: () => (
|
||||
<Button onClick={authorize}>
|
||||
<Button onClick={() => authorize()}>
|
||||
Authorize with ReviewDB
|
||||
</Button>
|
||||
)
|
||||
|
@ -53,38 +54,40 @@ export const settings = definePluginSettings({
|
|||
description: "Hide reviews from blocked users",
|
||||
default: true,
|
||||
},
|
||||
manageBlocks: {
|
||||
buttons: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Manage Blocked Users",
|
||||
description: "ReviewDB buttons",
|
||||
component: () => (
|
||||
<Button onClick={openBlockModal}>Manage Blocked Users</Button>
|
||||
)
|
||||
},
|
||||
website: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "ReviewDB website",
|
||||
component: () => (
|
||||
<Button onClick={async () => {
|
||||
let url = "https://reviewdb.mantikafasi.dev/";
|
||||
const token = await getToken();
|
||||
if (token)
|
||||
url += "/api/redirect?token=" + encodeURIComponent(token);
|
||||
<div className={cl("button-grid")} >
|
||||
<Button onClick={openBlockModal}>Manage Blocked Users</Button>
|
||||
|
||||
VencordNative.native.openExternal(url);
|
||||
}}>
|
||||
ReviewDB website
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
supportServer: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "ReviewDB Support Server",
|
||||
component: () => (
|
||||
<Button onClick={() => {
|
||||
VencordNative.native.openExternal("https://discord.gg/eWPBSbvznt");
|
||||
}}>
|
||||
ReviewDB Support Server
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
onClick={() => {
|
||||
VencordNative.native.openExternal("https://github.com/sponsors/mantikafasi");
|
||||
}}
|
||||
>
|
||||
Support ReviewDB development
|
||||
</Button>
|
||||
|
||||
<Button onClick={async () => {
|
||||
let url = "https://reviewdb.mantikafasi.dev/";
|
||||
const token = await getToken();
|
||||
if (token)
|
||||
url += "/api/redirect?token=" + encodeURIComponent(token);
|
||||
|
||||
VencordNative.native.openExternal(url);
|
||||
}}>
|
||||
ReviewDB website
|
||||
</Button>
|
||||
|
||||
|
||||
<Button onClick={() => {
|
||||
VencordNative.native.openExternal("https://discord.gg/eWPBSbvznt");
|
||||
}}>
|
||||
ReviewDB Support Server
|
||||
</Button>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
}).withPrivateSettings<{
|
||||
|
|
|
@ -59,8 +59,14 @@
|
|||
}
|
||||
|
||||
.vc-rdb-review {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-top: 8px !important;
|
||||
padding-bottom: 8px !important;
|
||||
padding-right: 32px !important;
|
||||
}
|
||||
|
||||
.vc-rdb-review:hover {
|
||||
background: var(--background-message-hover) !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vc-rdb-review-comment img {
|
||||
|
@ -91,6 +97,19 @@
|
|||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.vc-rdb-button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line media-feature-range-notation */
|
||||
@media (max-width: 600px) {
|
||||
.vc-rdb-button-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal-row {
|
||||
display: flex;
|
||||
height: 2em;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { UserStore } from "@webpack/common";
|
||||
import { Toasts, UserStore } from "@webpack/common";
|
||||
|
||||
import { Auth } from "./auth";
|
||||
import { Review, UserType } from "./entities";
|
||||
|
@ -41,3 +41,14 @@ export function canBlockReviewAuthor(profileId: string, review: Review) {
|
|||
export function canReportReview(review: Review) {
|
||||
return review.sender.discordID !== UserStore.getCurrentUser().id;
|
||||
}
|
||||
|
||||
export function showToast(message: string, type = Toasts.Type.MESSAGE) {
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message,
|
||||
type,
|
||||
options: {
|
||||
position: Toasts.Position.BOTTOM, // NOBODY LIKES TOASTS AT THE TOP
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,6 +46,23 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
const MEDIA_PROXY_URL = "https://media.discordapp.net";
|
||||
const CDN_URL = "https://cdn.discordapp.com";
|
||||
|
||||
function fixImageUrl(urlString: string, explodeWebp: boolean) {
|
||||
const url = new URL(urlString);
|
||||
if (url.origin === CDN_URL) return urlString;
|
||||
if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname;
|
||||
|
||||
url.searchParams.delete("width");
|
||||
url.searchParams.delete("height");
|
||||
url.searchParams.set("quality", "lossless");
|
||||
if (explodeWebp && url.searchParams.get("format") === "webp")
|
||||
url.searchParams.set("format", "png");
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "WebContextMenus",
|
||||
description: "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)",
|
||||
|
@ -182,34 +199,40 @@ export default definePlugin({
|
|||
],
|
||||
|
||||
async copyImage(url: string) {
|
||||
if (IS_VESKTOP && VesktopNative.clipboard) {
|
||||
const data = await fetch(url).then(r => r.arrayBuffer());
|
||||
VesktopNative.clipboard.copyImage(data, url);
|
||||
return;
|
||||
url = fixImageUrl(url, true);
|
||||
|
||||
let imageData = await fetch(url).then(r => r.blob());
|
||||
if (imageData.type !== "image/png") {
|
||||
const bitmap = await createImageBitmap(imageData);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = bitmap.width;
|
||||
canvas.height = bitmap.height;
|
||||
canvas.getContext("2d")!.drawImage(bitmap, 0, 0);
|
||||
|
||||
await new Promise<void>(done => {
|
||||
canvas.toBlob(data => {
|
||||
imageData = data!;
|
||||
done();
|
||||
}, "image/png");
|
||||
});
|
||||
}
|
||||
|
||||
// Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png
|
||||
// via canvas first
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
canvas.getContext("2d")!.drawImage(img, 0, 0);
|
||||
|
||||
canvas.toBlob(data => {
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
"image/png": data!
|
||||
})
|
||||
]);
|
||||
}, "image/png");
|
||||
};
|
||||
img.crossOrigin = "anonymous";
|
||||
img.src = url;
|
||||
if (IS_VESKTOP && VesktopNative.clipboard) {
|
||||
VesktopNative.clipboard.copyImage(await imageData.arrayBuffer(), url);
|
||||
return;
|
||||
} else {
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
"image/png": imageData
|
||||
})
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
async saveImage(url: string) {
|
||||
url = fixImageUrl(url, false);
|
||||
|
||||
const data = await fetchImage(url);
|
||||
if (!data) return;
|
||||
|
||||
|
|
|
@ -411,6 +411,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Samwich",
|
||||
id: 976176454511509554n,
|
||||
},
|
||||
coolelectronics: {
|
||||
name: "coolelectronics",
|
||||
id: 696392247205298207n,
|
||||
}
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
Loading…
Reference in a new issue