diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 99854bc66e..2174f2ee1f 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -33,27 +33,23 @@ import { onMounted, useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import Cropper from 'cropperjs'; import tinycolor from 'tinycolor2'; -import { apiUrl } from '@@/js/config.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import * as os from '@/os.js'; -import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { getProxiedImageUrl } from '@/utility/media-proxy.js'; -import { prefer } from '@/preferences.js'; - -const emit = defineEmits<{ - (ev: 'ok', cropped: Misskey.entities.DriveFile): void; - (ev: 'cancel'): void; - (ev: 'closed'): void; -}>(); const props = defineProps<{ - file: Misskey.entities.DriveFile; + imageFile: File; aspectRatio: number; uploadFolder?: string | null; }>(); -const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); +const emit = defineEmits<{ + (ev: 'ok', cropped: File | Blob): void; + (ev: 'cancel'): void; + (ev: 'closed'): void; +}>(); + +const imgUrl = URL.createObjectURL(props.imageFile); const dialogEl = useTemplateRef('dialogEl'); const imgEl = useTemplateRef('imgEl'); let cropper: Cropper | null = null; @@ -71,26 +67,7 @@ const ok = async () => { const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender }); croppedCanvas?.toBlob(blob => { if (!blob) return; - const formData = new FormData(); - formData.append('file', blob); - formData.append('name', `cropped_${props.file.name}`); - formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false'); - if (props.file.comment) { formData.append('comment', props.file.comment);} - formData.append('i', $i!.token); - if (props.uploadFolder) { - formData.append('folderId', props.uploadFolder); - } else if (props.uploadFolder !== null && prefer.s.uploadFolder) { - formData.append('folderId', prefer.s.uploadFolder); - } - - window.fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: formData, - }) - .then(response => response.json()) - .then(f => { - res(f); - }); + res(blob); }); }); diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index edbde654ec..95a617f491 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -148,7 +148,7 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/utility/achievements.js'; import { prefer } from '@/preferences.js'; -import { chooseFileFromPc } from '@/utility/select-file.js'; +import { chooseFileFromPcAndUpload } from '@/utility/select-file.js'; import { store } from '@/store.js'; import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js'; import { usePagination } from '@/composables/use-pagination.js'; @@ -555,7 +555,7 @@ function getMenu() { text: i18n.ts.upload, icon: 'ti ti-upload', action: () => { - chooseFileFromPc(true, { uploadFolder: folder.value?.id, keepOriginal: true }); + chooseFileFromPcAndUpload(true, { uploadFolder: folder.value?.id, keepOriginal: true }); }, }, { text: i18n.ts.fromUrl, diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index e8404cbd4f..a35eb1abf8 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -145,7 +145,7 @@ async function describe(file: Misskey.entities.DriveFile) { async function crop(file: Misskey.entities.DriveFile): Promise { if (mock) return; - const newFile = await os.cropImage(file, { aspectRatio: NaN }); + const newFile = await os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: NaN }); emit('replaceFile', file, newFile); } diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 30925b854c..a0d7376c8e 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -37,7 +37,7 @@ import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import FormSlot from '@/components/form/slot.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { chooseFileFromPc } from '@/utility/select-file.js'; +import { chooseFileFromPcAndUpload } from '@/utility/select-file.js'; import * as os from '@/os.js'; import { ensureSignin } from '@/i.js'; @@ -49,7 +49,7 @@ const description = ref($i.description ?? ''); watch(name, () => { os.apiWithDialog('i/update', { // 空文字列をnullにしたいので??は使うな - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + name: name.value || null, }, undefined, { '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': { @@ -62,13 +62,13 @@ watch(name, () => { watch(description, () => { os.apiWithDialog('i/update', { // 空文字列をnullにしたいので??は使うな - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description: description.value || null, }); }); function setAvatar(ev) { - chooseFileFromPc(false).then(async (files) => { + os.chooseFileFromPc({ multiple: false }).then(async (files) => { const file = files[0]; let originalOrCropped = file; @@ -81,13 +81,15 @@ function setAvatar(ev) { }); if (!canceled) { - originalOrCropped = await os.cropImage(file, { + originalOrCropped = await os.cropImageFile(file, { aspectRatio: 1, }); } + const driveFile = (await os.launchUploader([originalOrCropped], {}))[0]; + const i = await os.apiWithDialog('i/update', { - avatarId: originalOrCropped.id, + avatarId: driveFile.id, }); $i.avatarId = i.avatarId; $i.avatarUrl = i.avatarUrl; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 6d58307ea0..e93d7614b4 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -8,6 +8,7 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue'; import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; +import { getProxiedImageUrl } from './utility/media-proxy.js'; import type { Component, Ref } from 'vue'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; @@ -653,15 +654,13 @@ export async function pickEmoji(src: HTMLElement, opts: ComponentProps { +}): Promise { return new Promise(resolve => { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { - file: image, + imageFile: imageFile, aspectRatio: options.aspectRatio, - uploadFolder: options.uploadFolder, }, { ok: x => { resolve(x); @@ -671,6 +670,29 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: { }); } +export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: { + aspectRatio: number; + uploadFolder?: string | null; +}): Promise { + return new Promise(resolve => { + const imgUrl = getProxiedImageUrl(imageDriveFile.url, undefined, true); + const image = new Image(); + image.src = imgUrl; + image.onload = () => { + const canvas = window.document.createElement('canvas'); + const ctx = canvas.getContext('2d')!; + canvas.width = image.width; + canvas.height = image.height; + ctx.drawImage(image, 0, 0); + const imageFile = new File([canvas.toBlob()], imageDriveFile.name, { type: imageDriveFile.type }); + + cropImageFile(imageFile).then(croppedImageFile => { + + }); + }; + }); +} + export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: { align?: string; width?: number; @@ -774,6 +796,32 @@ export function checkExistence(fileData: ArrayBuffer): Promise { }); }*/ +export function chooseFileFromPc( + options: { + multiple?: boolean; + } = {}, +): Promise { + return new Promise((res, rej) => { + const input = window.document.createElement('input'); + input.type = 'file'; + input.multiple = options.multiple ?? false; + input.onchange = () => { + if (!input.files) return res([]); + + res(Array.from(input.files)); + + // 一応廃棄 + (window as any).__misskey_input_ref__ = null; + }; + + // https://qiita.com/fukasawah/items/b9dc732d95d99551013d + // iOS Safari で正常に動かす為のおまじない + (window as any).__misskey_input_ref__ = input; + + input.click(); + }); +} + export function launchUploader( files: File[], options?: { @@ -781,6 +829,7 @@ export function launchUploader( }, ): Promise { return new Promise((res, rej) => { + if (files.length === 0) return; const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), { files: markRaw(files), folderId: options?.folderId, diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index 6b11a181b0..a5345f8fc7 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -94,7 +94,7 @@ import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { validators } from '@/components/grid/cell-validators.js'; -import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js'; +import { chooseFileFromDrive, chooseFileFromPcAndUpload } from '@/utility/select-file.js'; import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; @@ -364,7 +364,7 @@ async function onDrop(ev: DragEvent) { } async function onFileSelectClicked() { - const driveFiles = await chooseFileFromPc( + const driveFiles = await chooseFileFromPcAndUpload( true, { uploadFolder: selectedFolderId.value, diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index fec260be72..a9511a9d17 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -130,7 +130,7 @@ function postThis() { function crop() { if (!file.value) return; - os.cropImage(file.value, { + os.createCroppedImageDriveFileFromImageDriveFile(file.value, { aspectRatio: NaN, uploadFolder: file.value.folderId ?? null, }); diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 30b7cf9a86..6ef93d36c0 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -268,7 +268,7 @@ function changeAvatar(ev) { }); if (!canceled) { - originalOrCropped = await os.cropImage(file, { + originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: 1, }); } @@ -294,7 +294,7 @@ function changeBanner(ev) { }); if (!canceled) { - originalOrCropped = await os.cropImage(file, { + originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: 2, }); } diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts index a5595cb0cb..b87b447e93 100644 --- a/packages/frontend/src/utility/get-drive-file-menu.ts +++ b/packages/frontend/src/utility/get-drive-file-menu.ts @@ -116,7 +116,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss menuItems.push({ text: i18n.ts.cropImage, icon: 'ti ti-crop', - action: () => os.cropImage(file, { + action: () => os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: NaN, uploadFolder: folder ? folder.id : folder, }), diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts index a5e458b9ff..b4d9efa3d5 100644 --- a/packages/frontend/src/utility/select-file.ts +++ b/packages/frontend/src/utility/select-file.ts @@ -11,34 +11,21 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; -export function chooseFileFromPc( +export function chooseFileFromPcAndUpload( options: { multiple?: boolean; folderId?: string | null; } = {}, ): Promise { return new Promise((res, rej) => { - const input = window.document.createElement('input'); - input.type = 'file'; - input.multiple = options.multiple ?? false; - input.onchange = () => { - if (!input.files) return res([]); - - os.launchUploader(Array.from(input.files), { + os.chooseFileFromPc({ multiple: options.multiple }).then(files => { + if (files.length === 0) return; + os.launchUploader(files, { folderId: options.folderId, }).then(driveFiles => { res(driveFiles); }); - - // 一応廃棄 - (window as any).__misskey_input_ref__ = null; - }; - - // https://qiita.com/fukasawah/items/b9dc732d95d99551013d - // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; - - input.click(); + }); }); } @@ -91,7 +78,7 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul } : undefined, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)), + action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)), }, { text: i18n.ts.fromDrive, icon: 'ti ti-cloud',