From ac4245dce1f2b957066ddc3cf10a1444fece7691 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight <saschanaz@outlook.com> Date: Wed, 5 Jul 2023 06:54:40 +0200 Subject: [PATCH] feat(frontend): allow cropping images on drive (#11092) * feat(frontend): allow cropping images on drive * nanka iroiro * folder --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp> --- .../src/components/MkCropperDialog.vue | 23 ++++++++++++------- .../frontend/src/components/MkDrive.file.vue | 5 ++-- packages/frontend/src/components/MkDrive.vue | 1 + .../frontend/src/components/MkPostForm.vue | 8 +++++-- .../src/components/MkPostFormAttaches.vue | 21 +++++++++++++---- packages/frontend/src/os.ts | 2 ++ .../src/scripts/get-drive-file-menu.ts | 13 +++++++++-- 7 files changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 82363499b7..b2d60d36c4 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -47,6 +47,7 @@ const emit = defineEmits<{ const props = defineProps<{ file: misskey.entities.DriveFile; aspectRatio: number; + uploadFolder?: string | null; }>(); const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); @@ -58,11 +59,17 @@ let loading = $ref(true); const ok = async () => { const promise = new Promise<misskey.entities.DriveFile>(async (res) => { const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas(); - croppedCanvas.toBlob(blob => { + croppedCanvas?.toBlob(blob => { + if (!blob) return; const formData = new FormData(); formData.append('file', blob); - formData.append('i', $i.token); - if (defaultStore.state.uploadFolder) { + formData.append('name', `cropped_${props.file.name}`); + formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false'); + formData.append('comment', props.file.comment ?? 'null'); + formData.append('i', $i!.token); + if (props.uploadFolder || props.uploadFolder === null) { + formData.append('folderId', props.uploadFolder ?? 'null'); + } else if (defaultStore.state.uploadFolder) { formData.append('folderId', defaultStore.state.uploadFolder); } @@ -82,12 +89,12 @@ const ok = async () => { const f = await promise; emit('ok', f); - dialogEl.close(); + dialogEl!.close(); }; const cancel = () => { emit('cancel'); - dialogEl.close(); + dialogEl!.close(); }; const onImageLoad = () => { @@ -100,7 +107,7 @@ const onImageLoad = () => { }; onMounted(() => { - cropper = new Cropper(imgEl, { + cropper = new Cropper(imgEl!, { }); const computedStyle = getComputedStyle(document.documentElement); @@ -112,13 +119,13 @@ onMounted(() => { selection.outlined = true; window.setTimeout(() => { - cropper.getCropperImage()!.$center('contain'); + cropper!.getCropperImage()!.$center('contain'); selection.$center(); }, 100); // モーダルオープンアニメーションãŒçµ‚ã‚ã£ãŸã‚ã¨ã§å†åº¦èª¿æ•´ window.setTimeout(() => { - cropper.getCropperImage()!.$center('contain'); + cropper!.getCropperImage()!.$center('contain'); selection.$center(); }, 500); }); diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index f0641161be..3a75f8293f 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -44,6 +44,7 @@ import { getDriveFileMenu } from '@/scripts/get-drive-file-menu'; const props = withDefaults(defineProps<{ file: Misskey.entities.DriveFile; + folder: Misskey.entities.DriveFolder | null; isSelected?: boolean; selectMode?: boolean; }>(), { @@ -65,12 +66,12 @@ function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } else { - os.popupMenu(getDriveFileMenu(props.file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } } function onContextmenu(ev: MouseEvent) { - os.contextMenu(getDriveFileMenu(props.file), ev); + os.contextMenu(getDriveFileMenu(props.file, props.folder), ev); } function onDragstart(ev: DragEvent) { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 52aef450d9..19508fe4de 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -65,6 +65,7 @@ v-anim="i" :class="$style.file" :file="file" + :folder="folder" :selectMode="select === 'file'" :isSelected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5c65569683..5b37a117de 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -66,7 +66,7 @@ <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> - <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> + <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/> <div v-if="showingOptions" style="padding: 8px 16px;"> @@ -410,7 +410,11 @@ function updateFileName(file, name) { files[files.findIndex(x => x.id === file.id)].name = name; } -function upload(file: File, name?: string) { +function replaceFile(file: misskey.entities.DriveFile, newFile: misskey.entities.DriveFile): void { + files[files.findIndex(x => x.id === file.id)] = newFile; +} + +function upload(file: File, name?: string): void { uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { files.push(res); }); diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 18fa142ebc..c50d025ab3 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -16,6 +16,7 @@ <script lang="ts" setup> import { defineAsyncComponent } from 'vue'; +import * as misskey from 'misskey-js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; @@ -30,8 +31,9 @@ const props = defineProps<{ const emit = defineEmits<{ (ev: 'update:modelValue', value: any[]): void; (ev: 'detach', id: string): void; - (ev: 'changeSensitive'): void; - (ev: 'changeName'): void; + (ev: 'changeSensitive', file: misskey.entities.DriveFile, isSensitive: boolean): void; + (ev: 'changeName', file: misskey.entities.DriveFile, newName: string): void; + (ev: 'replaceFile', file: misskey.entities.DriveFile, newFile: misskey.entities.DriveFile): void; }>(); let menuShowing = false; @@ -85,8 +87,15 @@ async function describe(file) { }, 'closed'); } -function showFileMenu(file, ev: MouseEvent) { +async function crop(file: misskey.entities.DriveFile): Promise<void> { + const newFile = await os.cropImage(file, { aspectRatio: NaN }); + emit('replaceFile', file, newFile); +} + +function showFileMenu(file: misskey.entities.DriveFile, ev: MouseEvent): void { if (menuShowing) return; + + const isImage = file.type.startsWith('image/'); os.popupMenu([{ text: i18n.ts.renameFile, icon: 'ti ti-forms', @@ -99,7 +108,11 @@ function showFileMenu(file, ev: MouseEvent) { text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => { describe(file); }, - }, { + }, ...isImage ? [{ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () : void => { crop(file); }, + }] : [], { text: i18n.ts.attachCancel, icon: 'ti ti-circle-x', action: () => { detachMedia(file.id); }, diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index c44d348046..1a5ed90541 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -460,11 +460,13 @@ export async function pickEmoji(src: HTMLElement | null, opts) { export async function cropImage(image: Misskey.entities.DriveFile, options: { aspectRatio: number; + uploadFolder?: string | null; }): Promise<Misskey.entities.DriveFile> { return new Promise((resolve, reject) => { popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { file: image, aspectRatio: options.aspectRatio, + uploadFolder: options.uploadFolder, }, { ok: x => { resolve(x); diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 060c8a1a11..ef0990b326 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -3,6 +3,7 @@ import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import * as os from '@/os'; +import { MenuItem } from '@/types/menu'; function rename(file: Misskey.entities.DriveFile) { os.inputText({ @@ -66,7 +67,8 @@ async function deleteFile(file: Misskey.entities.DriveFile) { }); } -export function getDriveFileMenu(file: Misskey.entities.DriveFile) { +export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { + const isImage = file.type.startsWith('image/'); return [{ text: i18n.ts.rename, icon: 'ti ti-forms', @@ -79,7 +81,14 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) { text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => describe(file), - }, null, { + }, ...isImage ? [{ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () => os.cropImage(file, { + aspectRatio: NaN, + uploadFolder: folder ? folder.id : folder + }), + }] : [], null, { text: i18n.ts.createNoteFromTheFile, icon: 'ti ti-pencil', action: () => os.post({ -- GitLab