FileUpload: add pixelvault host and fallback uploader order configuration (#1032)

* FileUpload: add pixelvault host
- also add optional configuration of fallback order

* FileUpload: improve UI of fallback order rows
This commit is contained in:
Key 2026-04-30 16:38:47 +01:00 • committed by GitHub
parent ba14a9ca87
commit 0cb8a34ec6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 270 additions and 20 deletions

View file

@ -376,6 +376,48 @@ export async function uploadToFilebin(
}
}
export async function uploadToPixelVault(
_: IpcMainInvokeEvent,
fileBuffer: ArrayBuffer,
filename: string,
uploadKey: string
): Promise<NativeUploadResult> {
try {
const formData = new FormData();
formData.append("file", new Blob([fileBuffer]), filename);
const response = await fetch("https://pixelvault.co/", {
method: "POST",
headers: {
Authorization: uploadKey
},
body: formData
});
const text = await response.text();
let data: { resource?: string; url?: string; } | null = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = null;
}
if (!response.ok) {
return { success: false, error: `Upload failed: ${response.status} ${text}` };
}
const url = data?.resource || data?.url || text.trim();
if (!url) {
return { success: false, error: "No URL returned from upload" };
}
return { success: true, url };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
}
}
export async function fetchFile(
_: IpcMainInvokeEvent,

View file

@ -8,14 +8,18 @@ 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 { ServiceType } from "./types";
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 },
@ -29,6 +33,7 @@ const serviceOptions = [
{ 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 }
];
@ -190,6 +195,18 @@ export const settings = definePluginSettings({
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",
@ -271,6 +288,72 @@ function SettingTextInput(props: {
);
}
function FallbackOrderSettings() {
const update = useForceUpdater();
const { store } = settings;
const [dragIndex, setDragIndex] = React.useState<number | null>(null);
const [order, setOrder] = React.useState<ServiceType[]>(() => {
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 (
<SettingsSection name="Fallback Order" description="Drag hosts to reorder fallback attempts. The selected host is tried first, then this order is used.">
<div className={cl("fallback-order-list")}>
{order.map((service, index) => (
<div
key={service}
className={cl("fallback-order-item")}
draggable
onDragStart={event => {
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}
>
<span className={cl("fallback-order-label")}>{serviceLabels[service]}</span>
<span className={cl("fallback-order-handle")}>Drag</span>
</div>
))}
</div>
<div className={cl("fallback-order-actions")}>
<Button size="small" onClick={() => commitOrder(fallbackServiceOrder)}>
Reset to default
</Button>
</div>
</SettingsSection>
);
}
export function SettingsComponent() {
const update = useForceUpdater();
const { store } = settings;
@ -282,6 +365,7 @@ export function SettingsComponent() {
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 = () => {
@ -482,6 +566,16 @@ export function SettingsComponent() {
/>
)}
{isPixelVault && (
<SettingTextInput
name="PixelVault Upload Key"
description="Your PixelVault authorization key"
value={store.pixelVaultKey}
onChange={v => store.pixelVaultKey = v}
placeholder="Your PixelVault upload key"
/>
)}
{isShareX && (
<>
<SettingsSection
@ -636,6 +730,8 @@ export function SettingsComponent() {
placeholder="Select timeout"
/>
</SettingsSection>
<FallbackOrderSettings />
</>
);
}

View file

@ -70,4 +70,39 @@
.vc-file-upload-progress-wrap[data-phase="success"] .vc-file-upload-progress-fill {
background: var(--status-positive);
}
}
.vc-file-upload-settings-fallback-order-list {
display: grid;
gap: 6px;
}
.vc-file-upload-settings-fallback-order-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--border-subtle);
background: var(--background-secondary);
color: var(--text-default);
cursor: grab;
}
.vc-file-upload-settings-fallback-order-item[data-dragging="true"] {
background: var(--background-modifier-hover);
}
.vc-file-upload-settings-fallback-order-label {
color: var(--text-default);
}
.vc-file-upload-settings-fallback-order-handle {
color: var(--text-muted);
font-size: 12px;
}
.vc-file-upload-settings-fallback-order-actions {
margin-top: 8px;
}

View file

@ -17,7 +17,8 @@ export enum ServiceType {
TMPFILES = "tmpfiles",
BUZZHEAVIER = "buzzheavier",
TEMPSH = "tempsh",
FILEBIN = "filebin"
FILEBIN = "filebin",
PIXELVAULT = "pixelvault"
}
export const serviceLabels: Record<ServiceType, string> = {
@ -33,9 +34,27 @@ export const serviceLabels: Record<ServiceType, string> = {
[ServiceType.TMPFILES]: "tmpfiles.org",
[ServiceType.BUZZHEAVIER]: "buzzheavier.com",
[ServiceType.TEMPSH]: "temp.sh",
[ServiceType.FILEBIN]: "filebin.net"
[ServiceType.FILEBIN]: "filebin.net",
[ServiceType.PIXELVAULT]: "PixelVault"
};
export const fallbackServiceOrder: ServiceType[] = [
ServiceType.ZIPLINE,
ServiceType.EZHOST,
ServiceType.NEST,
ServiceType.S3,
ServiceType.CATBOX,
ServiceType.ZEROX0,
ServiceType.LITTERBOX,
ServiceType.GOFILE,
ServiceType.TMPFILES,
ServiceType.BUZZHEAVIER,
ServiceType.TEMPSH,
ServiceType.FILEBIN,
ServiceType.PIXELVAULT,
ServiceType.SHAREX
];
export interface UploadResponse {
files: {
id: string;

View file

@ -6,7 +6,7 @@
import { normalizeCorsProxyUrl, toProxiedUrl } from "@equicordplugins/fileUpload/constants";
import { settings } from "@equicordplugins/fileUpload/settings";
import { serviceLabels, ServiceType, ShareXUploaderConfig, UploadResponse } from "@equicordplugins/fileUpload/types";
import { fallbackServiceOrder, serviceLabels, ServiceType, ShareXUploaderConfig, UploadResponse } from "@equicordplugins/fileUpload/types";
import { copyToClipboard } from "@utils/clipboard";
import { insertTextIntoChatInputBox } from "@utils/discord";
import { Logger } from "@utils/Logger";
@ -77,6 +77,18 @@ function isUploadCancelledError(error: unknown): boolean {
return message.includes("cancelled") || message.includes("canceled") || message.includes("aborted") || message.includes("aborterror");
}
function getFallbackServices(): ServiceType[] {
const configuredOrder = (settings.store as { fallbackOrder?: string; }).fallbackOrder || fallbackServiceOrder.join(",");
const services = configuredOrder
.split(/[\n,]/)
.map(service => service.trim())
.filter((service): service is ServiceType => Object.values(ServiceType).includes(service as ServiceType));
return services.length === fallbackServiceOrder.length && new Set(services).size === fallbackServiceOrder.length
? services
: fallbackServiceOrder;
}
function emitUploadState() {
for (const listener of uploadStateListeners) {
listener();
@ -363,6 +375,8 @@ export function isConfigured(): boolean {
case ServiceType.TEMPSH:
case ServiceType.FILEBIN:
return true;
case ServiceType.PIXELVAULT:
return Boolean((settings.store as { pixelVaultKey?: string; }).pixelVaultKey);
case ServiceType.SHAREX:
try {
parseShareXConfigFromSettings();
@ -647,6 +661,59 @@ async function uploadToFilebin(fileBlob: Blob, filename: string): Promise<string
return `https://filebin.net/${binId}/${encodeURIComponent(filename)}`;
}
async function uploadToPixelVault(fileBlob: Blob, filename: string): Promise<string> {
const { pixelVaultKey } = settings.store as { pixelVaultKey?: string; };
if (!pixelVaultKey?.trim()) {
throw new Error("PixelVault upload key is required");
}
if (Native) {
const result = await Native.uploadToPixelVault(await fileBlob.arrayBuffer(), filename, pixelVaultKey.trim());
if (!result.success) {
throw new Error(result.error || "Upload failed");
}
if (!result.url) {
throw new Error("No URL returned from upload");
}
return result.url;
}
const formData = new FormData();
formData.append("file", fileBlob, filename);
const response = await fetchWithTimeout("https://pixelvault.co/", {
method: "POST",
headers: {
Authorization: pixelVaultKey.trim()
},
body: formData
});
const text = await response.text();
let data: { resource?: string; url?: string; } | null = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = null;
}
if (!response.ok) {
throw new Error(`Upload failed: ${response.status} ${text}`);
}
const url = data?.resource || data?.url || text.trim();
if (!url) {
throw new Error("No URL returned from upload");
}
return url;
}
async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filename: string): Promise<string> {
switch (serviceType) {
case ServiceType.ZIPLINE:
@ -675,22 +742,13 @@ async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filenam
return uploadToTempSh(fileBlob, filename);
case ServiceType.FILEBIN:
return uploadToFilebin(fileBlob, filename);
case ServiceType.PIXELVAULT:
return uploadToPixelVault(fileBlob, filename);
default:
throw new Error("Unknown service type");
}
}
const FALLBACK_SERVICES: ServiceType[] = [
ServiceType.CATBOX,
ServiceType.LITTERBOX,
ServiceType.ZEROX0,
ServiceType.TMPFILES,
ServiceType.GOFILE,
ServiceType.BUZZHEAVIER,
ServiceType.TEMPSH,
ServiceType.FILEBIN
];
const EXE_BLOCKED_SERVICES = new Set<ServiceType>([
ServiceType.CATBOX,
ServiceType.LITTERBOX,
@ -768,12 +826,12 @@ function buildUploadOrder(primary: ServiceType, fileName: string): ServiceType[]
const disableFallbacks = Boolean((settings.store as { disableFallbacks?: boolean; }).disableFallbacks);
const effectivePrimary = normalizePrimaryService(primary, fileName);
if (disableFallbacks || effectivePrimary === ServiceType.SHAREX || effectivePrimary === ServiceType.S3 || effectivePrimary === ServiceType.ZIPLINE || effectivePrimary === ServiceType.NEST || effectivePrimary === ServiceType.EZHOST) {
return [effectivePrimary];
const order: ServiceType[] = [effectivePrimary];
if (disableFallbacks) {
return order;
}
const order: ServiceType[] = [effectivePrimary];
for (const fallback of FALLBACK_SERVICES) {
for (const fallback of getFallbackServices()) {
if (fallback !== effectivePrimary && canServiceHandleFile(fallback, fileName)) {
order.push(fallback);
}