firefish/packages/client/src/scripts/upload.ts

165 lines
4.3 KiB
TypeScript

import { reactive, ref } from "vue";
import type { entities } from "firefish-js";
import { readAndCompressImage } from "browser-image-resizer";
import { defaultStore } from "@/store";
import { apiUrl } from "@/config";
import { me } from "@/me";
import { alert } from "@/os";
import { i18n } from "@/i18n";
interface Uploading {
id: string;
name: string;
progressMax: number | undefined;
progressValue: number | undefined;
img: string;
}
export const uploads = ref<Uploading[]>([]);
const compressTypeMap = {
"image/jpeg": { quality: 0.85, mimeType: "image/jpeg" },
"image/png": { quality: 0.85, mimeType: "image/jpeg" },
"image/webp": { quality: 0.85, mimeType: "image/png" },
"image/svg+xml": { quality: 1, mimeType: "image/png" },
} as const;
const mimeTypeMap = {
"image/webp": "webp",
"image/jpeg": "jpg",
"image/png": "png",
"image/avif": "avif",
} as const;
export function uploadFile(
file: File,
folder?: any,
name?: string,
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
): Promise<entities.DriveFile> {
if (folder && typeof folder === "object") folder = folder.id;
return new Promise((resolve, reject) => {
const id = Math.random().toString();
const reader = new FileReader();
reader.onload = async (ev) => {
const ctx = reactive<Uploading>({
id,
name: name || file.name || "untitled",
progressMax: undefined,
progressValue: undefined,
img: window.URL.createObjectURL(file),
});
uploads.value.push(ctx);
let resizedImage: Blob | undefined;
if (!keepOriginal && file.type in compressTypeMap) {
const imgConfig = compressTypeMap[file.type];
const config = {
maxWidth: 2048,
maxHeight: 2048,
debug: true,
...imgConfig,
};
try {
resizedImage = await readAndCompressImage(file, config);
if (resizedImage.size > file.size) {
// if compression made the picture larger, give up compression
resizedImage = undefined;
} else {
ctx.name =
file.type !== imgConfig.mimeType
? `${ctx.name}.${
mimeTypeMap[compressTypeMap[file.type].mimeType]
}`
: ctx.name;
}
} catch (err) {
console.error("Failed to resize image", err);
}
}
const formData = new FormData();
formData.append("force", "true");
formData.append("file", resizedImage || file);
formData.append("name", ctx.name);
if (folder) formData.append("folderId", folder);
const xhr = new XMLHttpRequest();
xhr.open("POST", `${apiUrl}/drive/files/create`, true);
xhr.setRequestHeader("Authorization", `Bearer ${me.token}`);
xhr.onload = (ev) => {
if (
xhr.status !== 200 ||
ev.target == null ||
ev.target.response == null
) {
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
uploads.value = uploads.value.filter((x) => x.id !== id);
if (xhr.status === 413) {
alert({
type: "error",
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
});
} else if (ev.target?.response) {
const res = JSON.parse(ev.target.response);
if (res.error?.id === "bec5bd69-fba3-43c9-b4fb-2894b66ad5d2") {
alert({
type: "error",
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseInappropriate,
});
} else if (
res.error?.id === "d08dbc37-a6a9-463a-8c47-96c32ab5f064"
) {
alert({
type: "error",
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseNoFreeSpace,
});
} else {
alert({
type: "error",
title: i18n.ts.failedToUpload,
text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
});
}
} else {
alert({
type: "error",
title: "Failed to upload",
text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(
xhr.response,
)}`,
});
}
reject();
return;
}
const driveFile = JSON.parse(ev.target.response);
resolve(driveFile);
uploads.value = uploads.value.filter((x) => x.id !== id);
};
xhr.upload.onprogress = (ev) => {
if (ev.lengthComputable) {
ctx.progressMax = ev.total;
ctx.progressValue = ev.loaded;
}
};
xhr.send(formData);
};
reader.readAsArrayBuffer(file);
});
}