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>
This commit is contained in:
parent
1ab9f096c3
commit
ac4245dce1
7 changed files with 55 additions and 18 deletions
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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); },
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in a new issue