/* * Vencord, a Discord client mod * Copyright (c) 2024 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { definePluginSettings } from "@api/Settings"; import { Button } from "@components/Button"; import { SettingsSection } from "@components/settings/tabs/plugins/components/Common"; import { Switch } from "@components/Switch"; import { classNameFactory } from "@utils/css"; import { useForceUpdater } from "@utils/react"; import { OptionType } from "@utils/types"; import { React, Select, showToast, TextArea, TextInput, Toasts } from "@webpack/common"; import { CORS_PROXY } from "./constants"; import { fallbackServiceOrder, serviceLabels, ServiceType } from "./types"; import { parseShareXConfig } from "./utils/sharex"; const defaultFallbackOrder = fallbackServiceOrder.join(","); const cl = classNameFactory("vc-file-upload-settings-"); const serviceOptions = [ { label: "Zipline", value: ServiceType.ZIPLINE, default: true }, { label: "E-Z Host", value: ServiceType.EZHOST }, { label: "Nest", value: ServiceType.NEST }, { label: "S3-Compatible", value: ServiceType.S3 }, { label: "Catbox.moe", value: ServiceType.CATBOX }, ...(IS_DISCORD_DESKTOP ? [{ label: "0x0.st", value: ServiceType.ZEROX0 }] : []), { label: "Litterbox", value: ServiceType.LITTERBOX }, { label: "GoFile", value: ServiceType.GOFILE }, { label: "tmpfiles.org", value: ServiceType.TMPFILES }, { label: "buzzheavier.com", value: ServiceType.BUZZHEAVIER }, { label: "temp.sh", value: ServiceType.TEMPSH }, { label: "filebin.net", value: ServiceType.FILEBIN }, { label: "PixelVault", value: ServiceType.PIXELVAULT }, { label: "ShareX Custom Uploader", value: ServiceType.SHAREX } ]; const litterboxOptions = [ { label: "1 hour", value: "1h" }, { label: "12 hours", value: "12h" }, { label: "24 hours", value: "24h", default: true }, { label: "72 hours", value: "72h" } ]; const embedProxyOptions = [ { label: "CORS Proxy", value: "cors", default: true }, { label: "discord.nfp.is", value: "nfp" } ]; export const settings = definePluginSettings({ serviceType: { type: OptionType.SELECT, description: "Selected uploader service", options: serviceOptions, hidden: true }, serviceUrl: { type: OptionType.STRING, description: "Zipline service URL", default: "", hidden: true }, ziplineToken: { type: OptionType.STRING, description: "Zipline auth token", default: "", hidden: true }, folderId: { type: OptionType.STRING, description: "Optional Zipline folder ID", default: "", hidden: true }, ezHostKey: { type: OptionType.STRING, description: "E-Z Host API key", default: "", hidden: true }, nestToken: { type: OptionType.STRING, description: "Nest API token", default: "", hidden: true }, s3Endpoint: { type: OptionType.STRING, description: "S3-compatible endpoint URL", default: "", hidden: true }, s3Bucket: { type: OptionType.STRING, description: "S3 bucket name", default: "", hidden: true }, s3Region: { type: OptionType.STRING, description: "S3 region (use auto for R2)", default: "auto", hidden: true }, s3AccessKeyId: { type: OptionType.STRING, description: "S3 access key ID", default: "", hidden: true }, s3SecretAccessKey: { type: OptionType.STRING, description: "S3 secret access key", default: "", hidden: true }, s3SessionToken: { type: OptionType.STRING, description: "Optional S3 session token", default: "", hidden: true }, s3PublicUrl: { type: OptionType.STRING, description: "Optional public base URL", default: "", hidden: true }, s3Prefix: { type: OptionType.STRING, description: "Optional S3 object key prefix", default: "", hidden: true }, s3ForcePathStyle: { type: OptionType.BOOLEAN, description: "Use path-style S3 URLs", default: true, hidden: true }, litterboxExpiry: { type: OptionType.SELECT, description: "Litterbox retention window", options: litterboxOptions, default: "24h", hidden: true }, catboxUserhash: { type: OptionType.STRING, description: "Catbox userhash for account binding", default: "", hidden: true }, sharexConfig: { type: OptionType.STRING, description: "ShareX custom uploader JSON", default: "", hidden: true }, disableFallbacks: { type: OptionType.BOOLEAN, description: "Disable fallback upload services", default: false, hidden: true }, autoSend: { type: OptionType.BOOLEAN, description: "Insert uploaded URL in chat input", default: false, hidden: true }, autoFormat: { type: OptionType.BOOLEAN, description: "Wrap inserted URL in angle brackets", default: false, hidden: true }, bypassDiscordUpload: { type: OptionType.BOOLEAN, description: "Bypass Discord uploads and use FileUpload instead.", default: true, hidden: true }, bypassDiscordUploadOnlyOverLimit: { type: OptionType.BOOLEAN, description: "Only use FileUpload if the file(s) exceed the file size limit.", default: true, hidden: true }, gofileToken: { type: OptionType.STRING, description: "Optional GoFile API token", default: "", hidden: true }, fallbackOrder: { type: OptionType.STRING, description: "Fallback uploader order", default: defaultFallbackOrder, hidden: true }, pixelVaultKey: { type: OptionType.STRING, description: "PixelVault upload key", default: "", hidden: true }, uploadTimeoutMs: { type: OptionType.NUMBER, description: "Upload timeout in milliseconds", default: 300000, hidden: true }, stripQueryParams: { type: OptionType.BOOLEAN, description: "Strip query params from uploaded URLs", default: false, hidden: true }, embedProxyEnabled: { type: OptionType.BOOLEAN, description: "Proxy uploaded video links through an embed helper service.", default: false, hidden: true }, embedProxyService: { type: OptionType.SELECT, description: "Embed helper service to wrap uploaded video links.", options: embedProxyOptions, default: "cors", hidden: true }, corsProxyUrl: { type: OptionType.STRING, description: "CORS proxy URL used for browser uploads", default: CORS_PROXY, hidden: true }, apngToGif: { type: OptionType.BOOLEAN, description: "Convert APNG uploads to GIF", default: false, hidden: true }, preserveOriginalFilename: { type: OptionType.BOOLEAN, description: "Preserve the original filename when uploading.", default: true, hidden: true }, autoCopy: { type: OptionType.BOOLEAN, description: "Auto copy upload URL", default: true, hidden: true }, autoUploadPastedFiles: { type: OptionType.BOOLEAN, description: "Automatically upload files from clipboard to image host when pasting in chat input.", default: false }, settingsComponent: { type: OptionType.COMPONENT, description: "Settings", component: SettingsComponent } }); function SettingTextInput(props: { description: string; name: string; onChange: (value: string) => void; placeholder: string; value: string; }) { const { description, name, onChange, placeholder, value } = props; return ( ); } function FallbackOrderSettings() { const update = useForceUpdater(); const { store } = settings; const [dragIndex, setDragIndex] = React.useState(null); const [order, setOrder] = React.useState(() => { const configured = (store.fallbackOrder || defaultFallbackOrder) .split(/[\n,]/) .map(entry => entry.trim()) .filter((entry): entry is ServiceType => Object.values(ServiceType).includes(entry as ServiceType)); return configured.length === fallbackServiceOrder.length && new Set(configured).size === fallbackServiceOrder.length ? configured : fallbackServiceOrder; }); const commitOrder = (nextOrder: ServiceType[]) => { setOrder(nextOrder); store.fallbackOrder = nextOrder.join(","); update(); }; return (
{order.map((service, index) => (
{ setDragIndex(index); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", String(index)); }} onDragOver={event => event.preventDefault()} onDrop={event => { event.preventDefault(); const sourceIndex = dragIndex ?? Number(event.dataTransfer.getData("text/plain")); if (!Number.isInteger(sourceIndex) || sourceIndex === index || sourceIndex < 0 || sourceIndex >= order.length) { setDragIndex(null); return; } const nextOrder = [...order]; const [moved] = nextOrder.splice(sourceIndex, 1); nextOrder.splice(index, 0, moved); setDragIndex(null); commitOrder(nextOrder); }} onDragEnd={() => setDragIndex(null)} data-dragging={dragIndex === index} > {serviceLabels[service]} Drag
))}
); } export function SettingsComponent() { const update = useForceUpdater(); const { store } = settings; const sharexFileInputRef = React.useRef(null); const isNest = store.serviceType === ServiceType.NEST; const isEzHost = store.serviceType === ServiceType.EZHOST; const isS3 = store.serviceType === ServiceType.S3; const isZipline = store.serviceType === ServiceType.ZIPLINE; const isCatbox = store.serviceType === ServiceType.CATBOX; const isLitterbox = store.serviceType === ServiceType.LITTERBOX; const isGofile = store.serviceType === ServiceType.GOFILE; const isPixelVault = store.serviceType === ServiceType.PIXELVAULT; const isShareX = store.serviceType === ServiceType.SHAREX; const validateShareXConfig = () => { try { parseShareXConfig(store.sharexConfig || ""); showToast("ShareX config is valid", Toasts.Type.SUCCESS); } catch (error) { const message = error instanceof Error ? error.message : "Invalid ShareX config"; showToast(message, Toasts.Type.FAILURE); } }; const triggerShareXFileUpload = () => { sharexFileInputRef.current?.click(); }; const handleShareXFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e: ProgressEvent) => { try { const content = String(e.target?.result || ""); const parsed = parseShareXConfig(content); store.sharexConfig = JSON.stringify(parsed, null, 2); update(); showToast("Imported ShareX config", Toasts.Type.SUCCESS); } catch (error) { const message = error instanceof Error ? error.message : "Failed to import ShareX config"; showToast(message, Toasts.Type.FAILURE); } }; reader.readAsText(file); event.target.value = ""; }; return ( <> v === store.litterboxExpiry} select={v => { store.litterboxExpiry = v; update(); }} serialize={v => v} placeholder="Select expiry" /> )} {isGofile && ( store.gofileToken = v} placeholder="Optional GoFile token" /> )} {isPixelVault && ( store.pixelVaultKey = v} placeholder="Your PixelVault upload key" /> )} {isShareX && ( <>