wip
This commit is contained in:
parent
74c93fcebe
commit
9cd7ea77ff
|
@ -3,6 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
// https://vitejs.dev/config/build-options.html#build-modulepreload
|
// https://vitejs.dev/config/build-options.html#build-modulepreload
|
||||||
import 'vite/modulepreload-polyfill';
|
import 'vite/modulepreload-polyfill';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { inject, injectable, container } from 'tsyringe';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { defineAsyncComponent, reactive, ref } from 'vue';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
|
||||||
|
type Account = Misskey.entities.MeDetailed & { token: string };
|
||||||
|
|
||||||
|
const accountData = miLocalStorage.getItem('account');
|
||||||
|
|
||||||
|
const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class AccountService {
|
||||||
|
constructor(
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public readonly i = $i;
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { inject, injectable, container } from 'tsyringe';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||||
|
import { getCompressionConfig } from './upload/compress-config.js';
|
||||||
|
import { AccountService } from './AccountService.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { apiUrl } from '@/config.js';
|
||||||
|
import { alert } from '@/os.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
type Uploading = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
progressMax: number | undefined;
|
||||||
|
progressValue: number | undefined;
|
||||||
|
img: string;
|
||||||
|
};
|
||||||
|
export const uploads = ref<Uploading[]>([]);
|
||||||
|
|
||||||
|
const mimeTypeMap = {
|
||||||
|
'image/webp': 'webp',
|
||||||
|
'image/jpeg': 'jpg',
|
||||||
|
'image/png': 'png',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class Uploader {
|
||||||
|
constructor(
|
||||||
|
@inject('AccountService') private accountService: AccountService,
|
||||||
|
@inject('ServerMetadataService') private serverMetadataService: ServerMetadataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public uploadFile(
|
||||||
|
file: File,
|
||||||
|
folder?: any,
|
||||||
|
name?: string,
|
||||||
|
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
||||||
|
): Promise<Misskey.entities.DriveFile> {
|
||||||
|
if (this.accountService.i == null) throw new Error('Not logged in');
|
||||||
|
|
||||||
|
if (folder && typeof folder === 'object') folder = folder.id;
|
||||||
|
|
||||||
|
return fetchServerMetadata().then((serverMetadata) => new Promise((resolve, reject) => {
|
||||||
|
if (file.size > serverMetadata.maxFileSize) {
|
||||||
|
alert({
|
||||||
|
type: 'error',
|
||||||
|
title: i18n.ts.failedToUpload,
|
||||||
|
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
|
||||||
|
});
|
||||||
|
return reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (): Promise<void> => {
|
||||||
|
const filename = name ?? file.name ?? 'untitled';
|
||||||
|
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
|
||||||
|
|
||||||
|
const ctx = reactive<Uploading>({
|
||||||
|
id,
|
||||||
|
name: defaultStore.state.keepOriginalFilename ? filename : id + extension,
|
||||||
|
progressMax: undefined,
|
||||||
|
progressValue: undefined,
|
||||||
|
img: window.URL.createObjectURL(file),
|
||||||
|
});
|
||||||
|
|
||||||
|
uploads.value.push(ctx);
|
||||||
|
|
||||||
|
const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
|
||||||
|
let resizedImage: Blob | undefined;
|
||||||
|
if (config) {
|
||||||
|
try {
|
||||||
|
const resized = await readAndCompressImage(file, config);
|
||||||
|
if (resized.size < file.size || file.type === 'image/webp') {
|
||||||
|
// The compression may not always reduce the file size
|
||||||
|
// (and WebP is not browser safe yet)
|
||||||
|
resizedImage = resized;
|
||||||
|
}
|
||||||
|
if (_DEV_) {
|
||||||
|
const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
|
||||||
|
console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to resize image', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('i', this.accountService.i.token);
|
||||||
|
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.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
|
||||||
|
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);
|
||||||
|
}) as (ev: ProgressEvent<EventTarget>) => any;
|
||||||
|
|
||||||
|
xhr.upload.onprogress = ev => {
|
||||||
|
if (ev.lengthComputable) {
|
||||||
|
ctx.progressMax = ev.total;
|
||||||
|
ctx.progressValue = ev.loaded;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue