mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-19 22:20:34 +00:00
Merge f7e7895453
into b875ebf92d
This commit is contained in:
commit
39e82acc9a
9 changed files with 317 additions and 2 deletions
|
@ -93,7 +93,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
||||||
if (header) {
|
if (header) {
|
||||||
const csp = parsePolicy(headers[header][0]);
|
const csp = parsePolicy(headers[header][0]);
|
||||||
|
|
||||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src", "frame-src", "object-src"]) {
|
||||||
csp[directive] ??= [];
|
csp[directive] ??= [];
|
||||||
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
|
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
|
||||||
}
|
}
|
||||||
|
|
5
src/plugins/pdfViewer.desktop/README.md
Normal file
5
src/plugins/pdfViewer.desktop/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# PdfViewer
|
||||||
|
|
||||||
|
Allows you to Preview PDF Files without having to download them
|
||||||
|
|
||||||
|
![](https://github.com/user-attachments/assets/f403dff5-5edc-4e57-8503-73f709df4842)
|
48
src/plugins/pdfViewer.desktop/cache.ts
Normal file
48
src/plugins/pdfViewer.desktop/cache.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class LRUCache {
|
||||||
|
private cache: Map<string, string>;
|
||||||
|
private maxSize: number;
|
||||||
|
|
||||||
|
constructor(maxSize: number) {
|
||||||
|
this.cache = new Map();
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string): string | undefined {
|
||||||
|
if (!this.cache.has(key)) return undefined;
|
||||||
|
|
||||||
|
const value = this.cache.get(key)!;
|
||||||
|
this.cache.delete(key);
|
||||||
|
this.cache.set(key, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: string) {
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
} else if (this.cache.size >= this.maxSize) {
|
||||||
|
const oldestKey = this.cache.keys().next().value!;
|
||||||
|
this.cache.delete(oldestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: string) {
|
||||||
|
if (!this.cache.has(key)) return;
|
||||||
|
URL.revokeObjectURL(this.cache.get(key)!);
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
for (const key of this.cache.keys()) {
|
||||||
|
URL.revokeObjectURL(this.cache.get(key)!);
|
||||||
|
}
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
176
src/plugins/pdfViewer.desktop/index.tsx
Normal file
176
src/plugins/pdfViewer.desktop/index.tsx
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./pdfViewer.css";
|
||||||
|
|
||||||
|
import { get, set } from "@api/DataStore";
|
||||||
|
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||||
|
import { updateMessage } from "@api/MessageUpdater";
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType, PluginNative } from "@utils/types";
|
||||||
|
import { Icons, Spinner, Tooltip, useEffect, useState } from "@webpack/common";
|
||||||
|
|
||||||
|
import { LRUCache } from "./cache";
|
||||||
|
|
||||||
|
const Native = VencordNative.pluginHelpers.PdfViewer as PluginNative<typeof import("./native")>;
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
autoEmptyCache: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Automatically remove the cached PDF file when the component is unmounted. Turning this on will increase load times for PDFs that have already been viewed, but may consume less memory.",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
persistPreviewState: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Persist the state of opened/closed File Previews across channel switches and reloads.",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectUrlsCache = new LRUCache(20);
|
||||||
|
const STORE_KEY = "PdfViewer_PersistVisible";
|
||||||
|
|
||||||
|
interface Attachment {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
proxy_url: string;
|
||||||
|
content_type: string;
|
||||||
|
content_scan_version: number;
|
||||||
|
title: string;
|
||||||
|
spoiler: boolean;
|
||||||
|
previewBlobUrl?: string;
|
||||||
|
previewVisible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilePreview({ attachment }: { attachment: Attachment; }) {
|
||||||
|
const { previewBlobUrl, previewVisible } = attachment;
|
||||||
|
|
||||||
|
if (!previewVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"vc-pdf-viewer-container"}>
|
||||||
|
{previewBlobUrl ? <embed src={previewBlobUrl} className="vc-pdf-viewer-preview" title={attachment.filename} /> : <div style={{ display: "flex" }}><Spinner /></div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreviewButton({ attachment, channelId, messageId }: { attachment: Attachment; channelId: string; messageId: string; }) {
|
||||||
|
const [visible, setVisible] = useState<boolean | null>(null);
|
||||||
|
const [url, setUrl] = useState<string>();
|
||||||
|
|
||||||
|
const initPdfData = async () => {
|
||||||
|
const cachedUrl = objectUrlsCache.get(attachment.url);
|
||||||
|
if (cachedUrl) {
|
||||||
|
setUrl(cachedUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const buffer = await Native.getBufferResponse(attachment.url);
|
||||||
|
const file = new File([buffer], attachment.filename, { type: attachment.content_type });
|
||||||
|
|
||||||
|
const blobUrl = URL.createObjectURL(file);
|
||||||
|
objectUrlsCache.set(attachment.url, blobUrl);
|
||||||
|
setUrl(blobUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVisibility = async () => {
|
||||||
|
const data: Set<string> = await get(STORE_KEY) ?? new Set();
|
||||||
|
|
||||||
|
if (visible === null) {
|
||||||
|
setVisible(settings.store.persistPreviewState ? data.has(attachment.url) : false);
|
||||||
|
} else {
|
||||||
|
if (visible) data.add(attachment.url);
|
||||||
|
else data.delete(attachment.url);
|
||||||
|
|
||||||
|
await set(STORE_KEY, data);
|
||||||
|
|
||||||
|
attachment.previewVisible = visible;
|
||||||
|
updateMessage(channelId, messageId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateVisibility();
|
||||||
|
|
||||||
|
if (visible && !url) initPdfData();
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
attachment.previewBlobUrl = url;
|
||||||
|
updateMessage(channelId, messageId);
|
||||||
|
return () => {
|
||||||
|
if (url && settings.store.autoEmptyCache) {
|
||||||
|
objectUrlsCache.delete(attachment.url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return <Tooltip text={visible ? "Hide File Preview" : "Preview File"}>
|
||||||
|
{tooltipProps => (
|
||||||
|
<div
|
||||||
|
{...tooltipProps}
|
||||||
|
className="vc-pdf-viewer-toggle"
|
||||||
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
|
setVisible(v => !v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{visible ? <Icons.EyeSlashIcon /> : <Icons.EyeIcon />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "PdfViewer",
|
||||||
|
description: "Preview PDF Files without having to download them",
|
||||||
|
authors: [Devs.AGreenPig],
|
||||||
|
dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI",],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "Messages.IMG_ALT_ATTACHMENT_FILE_TYPE.format",
|
||||||
|
replacement: {
|
||||||
|
match: /newMosaicStyle,\i\),children:\[(?<=}=(\i);.+?)/,
|
||||||
|
replace: "$&$self.renderPreviewButton($1),"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addAccessory("pdfViewer", props => {
|
||||||
|
const pdfAttachments = props.message.attachments.filter(a => a.content_type === "application/pdf");
|
||||||
|
if (!pdfAttachments.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
{pdfAttachments.map((attachment, index) => (
|
||||||
|
<FilePreview key={index} attachment={attachment} />
|
||||||
|
))}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}, -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
objectUrlsCache.clear();
|
||||||
|
removeAccessory("pdfViewer");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreviewButton: ErrorBoundary.wrap(e => {
|
||||||
|
if (e.item.originalItem.content_type !== "application/pdf") return null;
|
||||||
|
return <PreviewButton attachment={e.item.originalItem} channelId={e.message.channel_id} messageId={e.message.id} />;
|
||||||
|
}, { noop: true }),
|
||||||
|
|
||||||
|
});
|
28
src/plugins/pdfViewer.desktop/native.ts
Normal file
28
src/plugins/pdfViewer.desktop/native.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IpcMainInvokeEvent } from "electron";
|
||||||
|
|
||||||
|
const urlChecks = [
|
||||||
|
(url: URL) => url.host === "cdn.discordapp.com",
|
||||||
|
(url: URL) => url.pathname.startsWith("/attachments/"),
|
||||||
|
(url: URL) => url.pathname.endsWith(".pdf")
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function getBufferResponse(_: IpcMainInvokeEvent, url: string): Promise<Buffer> {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
if (!urlChecks.every(check => check(urlObj))) {
|
||||||
|
throw new Error("Invalid URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
return Buffer.from(arrayBuffer);
|
||||||
|
}
|
30
src/plugins/pdfViewer.desktop/pdfViewer.css
Normal file
30
src/plugins/pdfViewer.desktop/pdfViewer.css
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
.vc-pdf-viewer-container {
|
||||||
|
resize: both;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 432px;
|
||||||
|
max-height: 80vh;
|
||||||
|
min-height: 500px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-pdf-viewer-preview {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-pdf-viewer-toggle {
|
||||||
|
justify-self: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-pdf-viewer-toggle:hover {
|
||||||
|
color: var(--interactive-hover);
|
||||||
|
}
|
|
@ -575,6 +575,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "RamziAH",
|
name: "RamziAH",
|
||||||
id: 1279957227612147747n,
|
id: 1279957227612147747n,
|
||||||
},
|
},
|
||||||
|
AGreenPig: {
|
||||||
|
name: "AGreenPig",
|
||||||
|
id: 427179231164760066n,
|
||||||
|
}
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
// iife so #__PURE__ works correctly
|
// iife so #__PURE__ works correctly
|
||||||
|
|
|
@ -51,6 +51,8 @@ export let ScrollerThin: t.ScrollerThin;
|
||||||
export let Clickable: t.Clickable;
|
export let Clickable: t.Clickable;
|
||||||
export let Avatar: t.Avatar;
|
export let Avatar: t.Avatar;
|
||||||
export let FocusLock: t.FocusLock;
|
export let FocusLock: t.FocusLock;
|
||||||
|
export let Spinner: t.Spinner;
|
||||||
|
export let SpinnerType: t.SpinnerType;
|
||||||
// token lagger real
|
// token lagger real
|
||||||
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
|
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
|
||||||
export let useToken: t.useToken;
|
export let useToken: t.useToken;
|
||||||
|
@ -84,7 +86,9 @@ waitFor(["FormItem", "Button"], m => {
|
||||||
Clickable,
|
Clickable,
|
||||||
Avatar,
|
Avatar,
|
||||||
FocusLock,
|
FocusLock,
|
||||||
Heading
|
Heading,
|
||||||
|
Spinner,
|
||||||
|
SpinnerType,
|
||||||
} = m);
|
} = m);
|
||||||
Forms = m;
|
Forms = m;
|
||||||
Icons = m;
|
Icons = m;
|
||||||
|
|
20
src/webpack/common/types/components.d.ts
vendored
20
src/webpack/common/types/components.d.ts
vendored
|
@ -505,9 +505,29 @@ type FocusLock = ComponentType<PropsWithChildren<{
|
||||||
containerRef: RefObject<HTMLElement>;
|
containerRef: RefObject<HTMLElement>;
|
||||||
}>>;
|
}>>;
|
||||||
|
|
||||||
|
export declare enum SpinnerType {
|
||||||
|
WANDERING_CUBES = "wanderingCubes",
|
||||||
|
CHASING_DOTS = "chasingDots",
|
||||||
|
PULSING_ELLIPSIS = "pulsingEllipsis",
|
||||||
|
SPINNING_CIRCLE = "spinningCircle",
|
||||||
|
SPINNING_CIRCLE_SIMPLE = "spinningCircleSimple",
|
||||||
|
LOW_MOTION = "lowMotion",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Spinner = ComponentType<{
|
||||||
|
type?: SpinnerType;
|
||||||
|
animated?: boolean;
|
||||||
|
className?: string;
|
||||||
|
itemClassName?: string;
|
||||||
|
"aria-label"?: string;
|
||||||
|
}> & {
|
||||||
|
Type: typeof SpinnerType;
|
||||||
|
};
|
||||||
|
|
||||||
export type Icon = ComponentType<JSX.IntrinsicElements["svg"] & {
|
export type Icon = ComponentType<JSX.IntrinsicElements["svg"] & {
|
||||||
size?: string;
|
size?: string;
|
||||||
colorClass?: string;
|
colorClass?: string;
|
||||||
} & Record<string, any>>;
|
} & Record<string, any>>;
|
||||||
|
|
||||||
export type Icons = Record<IconNames, Icon>;
|
export type Icons = Record<IconNames, Icon>;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue