diff --git a/src/equicordplugins/fileUpload/index.tsx b/src/equicordplugins/fileUpload/index.tsx
index 0cd895db2..724b5e926 100644
--- a/src/equicordplugins/fileUpload/index.tsx
+++ b/src/equicordplugins/fileUpload/index.tsx
@@ -104,6 +104,21 @@ function handlePaste(event: ClipboardEvent) {
void uploadProvidedFiles(files);
}
+function formatBytes(bytes: number): string {
+ if (!bytes) return "";
+
+ const units = ["B", "KB", "MB", "GB"];
+ let value = bytes;
+ let unitIndex = 0;
+
+ while (value >= 1024 && unitIndex < units.length - 1) {
+ value /= 1024;
+ unitIndex++;
+ }
+
+ return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
+}
+
const ProgressBarInner = () => {
const [state, setState] = useState(getUploadState);
@@ -112,6 +127,9 @@ const ProgressBarInner = () => {
if (state.phase === "idle") return null;
const percentage = Math.max(0, Math.min(100, state.percent));
+ const progressLabel = state.totalBytes > 0
+ ? `${Math.round(percentage)}% - ${formatBytes(state.transferredBytes)} of ${formatBytes(state.totalBytes)}`
+ : `${Math.round(percentage)}%`;
return (
{
{state.status || "Uploading..."}
+
+ {progressLabel}
+
{state.attempt > 0 && state.totalAttempts > 0 ? `${state.attempt}/${state.totalAttempts}` : ""}
diff --git a/src/equicordplugins/fileUpload/settings.tsx b/src/equicordplugins/fileUpload/settings.tsx
index 896a3e7df..d5451d3b3 100644
--- a/src/equicordplugins/fileUpload/settings.tsx
+++ b/src/equicordplugins/fileUpload/settings.tsx
@@ -285,7 +285,7 @@ function SettingTextInput(props: {
const { description, name, onChange, placeholder, value } = props;
return (
-
+
+
+ {children}
+
+
+ );
+}
+
+function SettingSwitch(props: {
+ checked: boolean;
+ description: string;
+ name: string;
+ onChange: (value: boolean) => void;
+}) {
+ const { checked, description, name, onChange } = props;
+
+ return (
+
+
+
+ );
+}
+
+function ServicePicker(props: {
+ onChange: (service: ServiceType) => void;
+ value: ServiceType;
+}) {
+ const { onChange, value } = props;
+
+ return (
+
+ {serviceOptions.map(option => (
+
+ ))}
+
+ );
+}
+
function FallbackOrderSettings() {
const update = useForceUpdater();
const { store } = settings;
@@ -413,21 +467,18 @@ export function SettingsComponent() {
return (
<>
-
-
-
+
@@ -620,134 +683,140 @@ export function SettingsComponent() {
onChange={handleShareXFileUpload}
/>
- >
+
)}
-
-
+ store.stripQueryParams = v}
/>
-
-
- {
store.embedProxyEnabled = v;
update();
}}
/>
-
- {store.embedProxyEnabled && (
-
-
- )}
+ {store.embedProxyEnabled && (
+
+
+ )}
-
store.corsProxyUrl = v}
- placeholder="https://your-cors-proxy.example.com"
- />
-
-
- codeberg.org/key/corsproxy
-
-
-
- store.apngToGif = v}
/>
-
-
- (store as { preserveOriginalFilename?: boolean; }).preserveOriginalFilename = v}
/>
-
-
- store.autoCopy = v}
/>
-
-
- store.disableFallbacks = v}
/>
-
-
- store.autoSend = v}
/>
-
-
- store.autoFormat = v}
/>
-
+
-
-
+ (store as { bypassDiscordUpload?: boolean; }).bypassDiscordUpload = v}
/>
-
-
- store.autoUploadPastedFiles = v}
/>
-
-
- store.bypassDiscordUploadOnlyOverLimit = v}
/>
-
+
-
-
+
+
+ codeberg.org/key/corsproxy
+
+
+
+
+
>
diff --git a/src/equicordplugins/fileUpload/styles.css b/src/equicordplugins/fileUpload/styles.css
index 609929b15..77a66fe50 100644
--- a/src/equicordplugins/fileUpload/styles.css
+++ b/src/equicordplugins/fileUpload/styles.css
@@ -32,6 +32,12 @@
color: var(--text-muted);
}
+.vc-file-upload-progress-percent {
+ font-size: 12px;
+ color: var(--text-default);
+ white-space: nowrap;
+}
+
.vc-file-upload-progress-cancel {
border: none;
border-radius: 6px;
@@ -106,3 +112,45 @@
.vc-file-upload-settings-fallback-order-actions {
margin-top: 8px;
}
+
+.vc-file-upload-settings-group {
+ display: grid;
+ gap: 10px;
+}
+
+.vc-file-upload-settings-service-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 8px;
+}
+
+.vc-file-upload-settings-service-option {
+ min-height: 38px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 8px;
+ background: var(--background-secondary);
+ color: var(--text-default);
+ cursor: pointer;
+ padding: 8px 10px;
+ text-align: left;
+}
+
+.vc-file-upload-settings-service-option:hover {
+ background: var(--background-modifier-hover);
+}
+
+.vc-file-upload-settings-service-option[data-selected="true"] {
+ border-color: var(--brand-500);
+ background: var(--background-modifier-selected);
+}
+
+.vc-file-upload-settings-service-option-label {
+ font-size: 13px;
+ font-weight: 500;
+}
+
+.vc-file-upload-settings-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
diff --git a/src/equicordplugins/fileUpload/utils/s3.ts b/src/equicordplugins/fileUpload/utils/s3.ts
index 2cc9827d3..092189973 100644
--- a/src/equicordplugins/fileUpload/utils/s3.ts
+++ b/src/equicordplugins/fileUpload/utils/s3.ts
@@ -21,6 +21,14 @@ type S3Store = {
corsProxyUrl?: string;
};
+type UploadRequestResponse = {
+ ok: boolean;
+ status: number;
+ text(): Promise;
+};
+
+type UploadRequest = (url: string, options: RequestInit) => Promise;
+
const textEncoder = new TextEncoder();
function toArrayBuffer(data: ArrayBuffer | Uint8Array | string): ArrayBuffer {
@@ -90,7 +98,8 @@ export function isS3Configured(): boolean {
export async function uploadToS3(
fileBlob: Blob,
filename: string,
- native: PluginNative | null
+ native: PluginNative | null,
+ uploadRequest: UploadRequest
): Promise {
const {
s3Endpoint,
@@ -189,7 +198,7 @@ export async function uploadToS3(
throw new Error(result.error || "Upload failed");
}
} else {
- const response = await fetch(toProxiedUrl(uploadUrl.toString(), settings.store.corsProxyUrl), {
+ const response = await uploadRequest(toProxiedUrl(uploadUrl.toString(), settings.store.corsProxyUrl), {
method: "PUT",
headers: requestHeaders,
body: fileBlob
diff --git a/src/equicordplugins/fileUpload/utils/upload.ts b/src/equicordplugins/fileUpload/utils/upload.ts
index 78d3518b3..fb0bfd5f5 100644
--- a/src/equicordplugins/fileUpload/utils/upload.ts
+++ b/src/equicordplugins/fileUpload/utils/upload.ts
@@ -48,6 +48,8 @@ export interface UploadProgressState {
attempt: number;
totalAttempts: number;
percent: number;
+ transferredBytes: number;
+ totalBytes: number;
status: string;
canCancel: boolean;
}
@@ -60,6 +62,8 @@ const defaultUploadState: UploadProgressState = {
attempt: 0,
totalAttempts: 0,
percent: 0,
+ transferredBytes: 0,
+ totalBytes: 0,
status: "",
canCancel: false
};
@@ -67,6 +71,7 @@ const defaultUploadState: UploadProgressState = {
let uploadState: UploadProgressState = { ...defaultUploadState };
const uploadStateListeners = new Set<() => void>();
let activeAbortController: AbortController | null = null;
+let activeXhr: XMLHttpRequest | null = null;
let cancelRequested = false;
function isUploadCancelledError(error: unknown): boolean {
@@ -121,6 +126,7 @@ export function cancelCurrentUpload() {
cancelRequested = true;
activeAbortController?.abort();
+ activeXhr?.abort();
setUploadState({
phase: "cancelled",
status: "Upload cancelled.",
@@ -163,6 +169,103 @@ async function fetchWithTimeout(url: string, options: RequestInit): Promise [key, value]);
+
+ return Object.entries(headers);
+}
+
+class XhrResponse {
+ ok: boolean;
+ headers: Headers;
+ status: number;
+ statusText: string;
+ url: string;
+
+ constructor(private xhr: XMLHttpRequest) {
+ this.status = xhr.status;
+ this.statusText = xhr.statusText;
+ this.url = xhr.responseURL;
+ this.ok = this.status >= 200 && this.status < 300;
+ this.headers = new Headers();
+
+ const rawHeaders = xhr.getAllResponseHeaders();
+ for (const line of rawHeaders.trim().split(/[\r\n]+/)) {
+ if (!line) continue;
+
+ const separatorIndex = line.indexOf(":");
+ if (separatorIndex < 0) continue;
+
+ this.headers.append(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
+ }
+ }
+
+ async text(): Promise {
+ return typeof this.xhr.response === "string"
+ ? this.xhr.response
+ : this.xhr.responseText;
+ }
+
+ async json(): Promise {
+ return JSON.parse(await this.text());
+ }
+}
+
+function setXhrUploadProgress(event: ProgressEvent) {
+ if (!event.lengthComputable || event.total <= 0) {
+ setUploadState({
+ status: uploadState.currentServiceLabel
+ ? `Uploading via ${uploadState.currentServiceLabel}...`
+ : "Uploading..."
+ });
+ return;
+ }
+
+ const percent = Math.round(Math.max(0, Math.min(100, event.loaded / event.total * 100)));
+ setUploadState({
+ phase: "uploading",
+ percent,
+ transferredBytes: event.loaded,
+ totalBytes: event.total,
+ status: uploadState.currentServiceLabel
+ ? `Uploading via ${uploadState.currentServiceLabel}...`
+ : "Uploading..."
+ });
+}
+
+async function uploadRequestWithTimeout(url: string, options: RequestInit): Promise {
+ const requestUrl = toProxyUrl(url);
+
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ activeXhr = xhr;
+ const timeout = setTimeout(() => xhr.abort(), getUploadTimeoutMs());
+
+ xhr.open(options.method || "GET", requestUrl);
+
+ for (const [key, value] of getHeaderEntries(options.headers)) {
+ xhr.setRequestHeader(key, value);
+ }
+
+ xhr.upload.onprogress = setXhrUploadProgress;
+ xhr.onload = () => resolve(new XhrResponse(xhr));
+ xhr.onerror = () => reject(new Error("Upload failed"));
+ xhr.onabort = () => reject(new Error(cancelRequested ? "Upload cancelled by user" : "Upload timed out"));
+ xhr.onloadend = () => {
+ clearTimeout(timeout);
+ xhr.upload.onprogress = null;
+ if (activeXhr === xhr) {
+ activeXhr = null;
+ }
+ };
+
+ const { body } = options;
+ xhr.send(body instanceof ReadableStream ? null : body as XMLHttpRequestBodyInit | null);
+ });
+}
+
function resolveShareXRequestValue(value: string | number | boolean, filename: string): string {
return String(value)
.replace(/\$filename\$/g, filename)
@@ -225,7 +328,7 @@ async function uploadToShareX(fileBlob: Blob, filename: string): Promise
throw new Error(`Unsupported ShareX Body type: ${config.Body || "unknown"}`);
}
- const response = await fetchWithTimeout(requestUrl, { method, headers, body });
+ const response = await uploadRequestWithTimeout(requestUrl, { method, headers, body });
const responseText = await response.text();
let responseJson: unknown = null;
@@ -272,7 +375,7 @@ async function uploadToZipline(fileBlob: Blob, filename: string): Promise 0 && data.files[0].url) {
return data.files[0].url;
@@ -323,7 +426,7 @@ async function uploadToNest(fileBlob: Blob, filename: string): Promise {
const formData = new FormData();
formData.append("file", fileBlob, filename);
- const response = await fetchWithTimeout("https://nest.rip/api/files/upload", {
+ const response = await uploadRequestWithTimeout("https://nest.rip/api/files/upload", {
method: "POST",
headers: {
"Authorization": nestToken
@@ -416,7 +519,7 @@ async function uploadToEzHost(fileBlob: Blob, filename: string): Promise
const headers: Record = { key: ezHostKey };
- const response = await fetchWithTimeout("https://api.e-z.host/files", {
+ const response = await uploadRequestWithTimeout("https://api.e-z.host/files", {
method: "POST",
headers,
body: formData
@@ -427,12 +530,14 @@ async function uploadToEzHost(fileBlob: Blob, filename: string): Promise
throw new Error(`Upload failed: ${response.status} ${text}`);
}
- const data = await response.json();
+ const data = await response.json() as { success?: boolean; error?: string; imageUrl?: string; rawUrl?: string; };
if (!data || !data.success) {
throw new Error(data?.error || "Upload failed");
}
- return data.imageUrl || data.rawUrl;
+ const url = data.imageUrl || data.rawUrl;
+ if (!url) throw new Error("No URL returned from upload");
+ return url;
}
async function uploadToCatbox(fileBlob: Blob, filename: string): Promise {
@@ -449,7 +554,7 @@ async function uploadToCatbox(fileBlob: Blob, filename: string): Promise
if (catboxUserhash) formData.append("userhash", catboxUserhash);
formData.append("fileToUpload", fileBlob, filename);
- const response = await fetchWithTimeout("https://catbox.moe/user/api.php", {
+ const response = await uploadRequestWithTimeout("https://catbox.moe/user/api.php", {
method: "POST",
body: formData
});
@@ -493,7 +598,7 @@ async function uploadToLitterbox(fileBlob: Blob, filename: string): Promise
formData.append("file", fileBlob, filename);
const uploadUrl = "https://upload.gofile.io/uploadfile";
- const response = await fetchWithTimeout(uploadUrl, {
+ const response = await uploadRequestWithTimeout(uploadUrl, {
method: "POST",
body: formData
});
@@ -555,7 +660,7 @@ async function uploadToTmpfiles(fileBlob: Blob, filename: string): Promise
formData.append("file", fileBlob, filename);
const uploadUrl = "https://temp.sh/upload";
- const response = await fetchWithTimeout(uploadUrl, {
+ const response = await uploadRequestWithTimeout(uploadUrl, {
method: "POST",
body: formData
});
@@ -650,7 +755,7 @@ async function uploadToFilebin(fileBlob: Blob, filename: string): Promise {
} finally {
isUploading = false;
activeAbortController = null;
+ activeXhr = null;
setTimeout(() => resetUploadState(), 1800);
}
}
@@ -1197,6 +1308,7 @@ export async function uploadProvidedFiles(files: readonly File[]): Promise
} finally {
isUploading = false;
activeAbortController = null;
+ activeXhr = null;
setTimeout(() => resetUploadState(), 1800);
}
}
diff --git a/src/equicordplugins/moreStickers/stickers.ts b/src/equicordplugins/moreStickers/stickers.ts
index a51e5e64b..0d3717f39 100644
--- a/src/equicordplugins/moreStickers/stickers.ts
+++ b/src/equicordplugins/moreStickers/stickers.ts
@@ -8,8 +8,7 @@ import * as DataStore from "@api/DataStore";
import { removeRecentStickerByPackId } from "./components";
import { DynamicPackSetMeta, DynamicStickerPackMeta, StickerPack, StickerPackMeta } from "./types";
-import { Mutex } from "./utils";
-import { corsFetch } from "./utils";
+import { corsFetch, Mutex } from "./utils";
const mutex = new Mutex();