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:
parent
ba14a9ca87
commit
0cb8a34ec6
5 changed files with 270 additions and 20 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue