enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション (#8216)
* wip * Update packages/client/src/os.ts Co-authored-by: tamaina <tamaina@hotmail.co.jp> * メニューをComposition API化、switchアイテム追加 クライアントサイド画像圧縮の準備 * メニュー型定義を分離 (TypeScriptの型支援が効かないので) * disabled * make keepOriginal to follow setting value * ✌️ * fix * fix * ✌️ * WEBP * aaa * ✌️ * webp * lazy load browser-image-resizer * rename * rename 2 * Fix * clean up * add comment * clean up * jpeg, pngにもどす * fix * fix name * webpでなくする ただしサムネやプレビューはwebpのまま (テスト) * 動画サムネイルはjpegに * エラーハンドリング * ✌️ * v2.2.1-misskey-beta.2 * browser-image-resizer#v2.2.1-misskey.1 * ✌️ * fix alert * update browser-image-resizer to v2.2.1-misskey.2 * lockfile Co-authored-by: mei23 <m@m544.net> Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>
This commit is contained in:
@ -97,6 +97,7 @@ import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { uploadFile, uploads } from '@/scripts/upload';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder;
|
||||
@ -127,8 +128,9 @@ const moreFolders = ref(false);
|
||||
const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
||||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const uploadings = os.uploads;
|
||||
const uploadings = uploads;
|
||||
const connection = stream.useChannel('drive');
|
||||
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
|
||||
// ドロップされようとしているか
|
||||
const draghover = ref(false);
|
||||
@ -355,7 +357,7 @@ function onChangeFileInput() {
|
||||
}
|
||||
|
||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
|
||||
os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
|
||||
uploadFile(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
|
||||
addFile(res, true);
|
||||
});
|
||||
}
|
||||
@ -562,6 +564,10 @@ function fetchMoreFiles() {
|
||||
|
||||
function getMenu() {
|
||||
return [{
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
}, null, {
|
||||
text: i18n.ts.addFile,
|
||||
type: 'label'
|
||||
}, {
|
||||
|
@ -87,6 +87,7 @@ import MkInfo from '@/components/ui/info.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
@ -372,7 +373,7 @@ function updateFileName(file, name) {
|
||||
}
|
||||
|
||||
function upload(file: File, name?: string) {
|
||||
os.upload(file, defaultStore.state.uploadFolder, name).then(res => {
|
||||
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
|
||||
files.push(res);
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||
|
||||
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
|
||||
import { Component, markRaw, Ref, ref } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import * as Misskey from 'misskey-js';
|
||||
@ -10,7 +10,6 @@ import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import { resolve } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
|
||||
@ -537,78 +536,6 @@ export function post(props: Record<string, any> = {}) {
|
||||
|
||||
export const deckGlobalEvents = new EventEmitter();
|
||||
|
||||
export const uploads = ref<{
|
||||
id: string;
|
||||
name: string;
|
||||
progressMax: number | undefined;
|
||||
progressValue: number | undefined;
|
||||
img: string;
|
||||
}[]>([]);
|
||||
|
||||
export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.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 = (e) => {
|
||||
const ctx = reactive({
|
||||
id: id,
|
||||
name: name || file.name || 'untitled',
|
||||
progressMax: undefined,
|
||||
progressValue: undefined,
|
||||
img: window.URL.createObjectURL(file)
|
||||
});
|
||||
|
||||
uploads.value.push(ctx);
|
||||
|
||||
console.log(keepOriginal);
|
||||
|
||||
const data = new FormData();
|
||||
data.append('i', $i.token);
|
||||
data.append('force', 'true');
|
||||
data.append('file', file);
|
||||
|
||||
if (folder) data.append('folderId', folder);
|
||||
if (name) data.append('name', name);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = (ev) => {
|
||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||
// TODO: 消すのではなくて再送できるようにしたい
|
||||
uploads.value = uploads.value.filter(x => x.id != id);
|
||||
|
||||
alert({
|
||||
type: 'error',
|
||||
text: 'upload failed'
|
||||
});
|
||||
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const driveFile = JSON.parse(ev.target.response);
|
||||
|
||||
resolve(driveFile);
|
||||
|
||||
uploads.value = uploads.value.filter(x => x.id != id);
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = e => {
|
||||
if (e.lengthComputable) {
|
||||
ctx.progressMax = e.total;
|
||||
ctx.progressValue = e.loaded;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(data);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -31,6 +31,7 @@ import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { Autocomplete } from '@/scripts/autocomplete';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@ -164,7 +165,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
upload(file: File, name?: string) {
|
||||
os.upload(file, this.$store.state.uploadFolder, name).then(res => {
|
||||
uploadFile(file, this.$store.state.uploadFolder, name).then(res => {
|
||||
this.file = res;
|
||||
});
|
||||
},
|
||||
|
@ -4,6 +4,7 @@ import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { DriveFile } from 'misskey-js/built/entities';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
@ -14,7 +15,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
||||
input.type = 'file';
|
||||
input.multiple = multiple;
|
||||
input.onchange = () => {
|
||||
const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
|
||||
const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
|
||||
|
||||
Promise.all(promises).then(driveFiles => {
|
||||
res(multiple ? driveFiles : driveFiles[0]);
|
||||
|
114
packages/client/src/scripts/upload.ts
Normal file
114
packages/client/src/scripts/upload.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { apiUrl } from '@/config';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { $i } from '@/account';
|
||||
import { readAndCompressImage } from 'browser-image-resizer';
|
||||
import { alert } from '@/os';
|
||||
|
||||
type 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/webp': { quality: 0.85, mimeType: 'image/jpeg' },
|
||||
'image/svg+xml': { quality: 1, mimeType: 'image/png' },
|
||||
} as const;
|
||||
|
||||
const mimeTypeMap = {
|
||||
'image/webp': 'webp',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
} as const;
|
||||
|
||||
export function uploadFile(
|
||||
file: File,
|
||||
folder?: any,
|
||||
name?: string,
|
||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading
|
||||
): Promise<Misskey.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 (e) => {
|
||||
const ctx = reactive<Uploading>({
|
||||
id: id,
|
||||
name: name || file.name || 'untitled',
|
||||
progressMax: undefined,
|
||||
progressValue: undefined,
|
||||
img: window.URL.createObjectURL(file)
|
||||
});
|
||||
|
||||
uploads.value.push(ctx);
|
||||
|
||||
let resizedImage: any;
|
||||
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);
|
||||
ctx.name = file.type !== imgConfig.mimeType ? `${ctx.name}.${mimeTypeMap[compressTypeMap[file.type].mimeType]}` : ctx.name;
|
||||
} catch (e) {
|
||||
console.error('Failed to resize image', e);
|
||||
}
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
data.append('i', $i.token);
|
||||
data.append('force', 'true');
|
||||
data.append('file', resizedImage || file);
|
||||
data.append('name', ctx.name);
|
||||
if (folder) data.append('folderId', folder);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = (ev) => {
|
||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||
// TODO: 消すのではなくて再送できるようにしたい
|
||||
uploads.value = uploads.value.filter(x => x.id != id);
|
||||
|
||||
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 = e => {
|
||||
if (e.lengthComputable) {
|
||||
ctx.progressMax = e.total;
|
||||
ctx.progressValue = e.loaded;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(data);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
@ -17,7 +17,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { popup, popups, uploads, pendingApiRequestsCount } from '@/os';
|
||||
import { popup, popups, pendingApiRequestsCount } from '@/os';
|
||||
import { uploads } from '@/scripts/upload';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import { $i } from '@/account';
|
||||
import { stream } from '@/stream';
|
||||
|
@ -20,8 +20,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import { uploads } from '@/scripts/upload';
|
||||
|
||||
const uploads = os.uploads;
|
||||
const zIndex = os.claimZIndex('high');
|
||||
</script>
|
||||
|
||||
|
Reference in New Issue
Block a user