mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-19 22:20:34 +00:00
Merge c274698b90
into 8afd79dd50
This commit is contained in:
commit
4f88753ac5
9 changed files with 312 additions and 2 deletions
|
@ -93,7 +93,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
|||
if (header) {
|
||||
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].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();
|
||||
}
|
||||
}
|
171
src/plugins/pdfViewer.desktop/index.tsx
Normal file
171
src/plugins/pdfViewer.desktop/index.tsx
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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);
|
||||
},
|
||||
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} />;
|
||||
}),
|
||||
stop() {
|
||||
objectUrlsCache.clear();
|
||||
removeAccessory("pdfViewer");
|
||||
}
|
||||
});
|
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",
|
||||
id: 1279957227612147747n,
|
||||
},
|
||||
AGreenPig: {
|
||||
name: "AGreenPig",
|
||||
id: 427179231164760066n,
|
||||
}
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
@ -51,6 +51,8 @@ export let ScrollerThin: t.ScrollerThin;
|
|||
export let Clickable: t.Clickable;
|
||||
export let Avatar: t.Avatar;
|
||||
export let FocusLock: t.FocusLock;
|
||||
export let Spinner: t.Spinner;
|
||||
export let SpinnerType: t.SpinnerType;
|
||||
// token lagger real
|
||||
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
|
||||
export let useToken: t.useToken;
|
||||
|
@ -84,7 +86,9 @@ waitFor(["FormItem", "Button"], m => {
|
|||
Clickable,
|
||||
Avatar,
|
||||
FocusLock,
|
||||
Heading
|
||||
Heading,
|
||||
Spinner,
|
||||
SpinnerType,
|
||||
} = m);
|
||||
Forms = 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>;
|
||||
}>>;
|
||||
|
||||
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"] & {
|
||||
size?: string;
|
||||
colorClass?: string;
|
||||
} & Record<string, any>>;
|
||||
|
||||
export type Icons = Record<IconNames, Icon>;
|
||||
|
||||
|
|
Loading…
Reference in a new issue