mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 15:34:13 +00:00 
			
		
		
		
	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<{
 | 
					const props = defineProps<{
 | 
				
			||||||
	file: misskey.entities.DriveFile;
 | 
						file: misskey.entities.DriveFile;
 | 
				
			||||||
	aspectRatio: number;
 | 
						aspectRatio: number;
 | 
				
			||||||
 | 
						uploadFolder?: string | null;
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
 | 
					const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
 | 
				
			||||||
| 
						 | 
					@ -58,11 +59,17 @@ let loading = $ref(true);
 | 
				
			||||||
const ok = async () => {
 | 
					const ok = async () => {
 | 
				
			||||||
	const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
 | 
						const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
 | 
				
			||||||
		const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
 | 
							const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
 | 
				
			||||||
		croppedCanvas.toBlob(blob => {
 | 
							croppedCanvas?.toBlob(blob => {
 | 
				
			||||||
 | 
								if (!blob) return;
 | 
				
			||||||
			const formData = new FormData();
 | 
								const formData = new FormData();
 | 
				
			||||||
			formData.append('file', blob);
 | 
								formData.append('file', blob);
 | 
				
			||||||
			formData.append('i', $i.token);
 | 
								formData.append('name', `cropped_${props.file.name}`);
 | 
				
			||||||
			if (defaultStore.state.uploadFolder) {
 | 
								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);
 | 
									formData.append('folderId', defaultStore.state.uploadFolder);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,12 +89,12 @@ const ok = async () => {
 | 
				
			||||||
	const f = await promise;
 | 
						const f = await promise;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emit('ok', f);
 | 
						emit('ok', f);
 | 
				
			||||||
	dialogEl.close();
 | 
						dialogEl!.close();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const cancel = () => {
 | 
					const cancel = () => {
 | 
				
			||||||
	emit('cancel');
 | 
						emit('cancel');
 | 
				
			||||||
	dialogEl.close();
 | 
						dialogEl!.close();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const onImageLoad = () => {
 | 
					const onImageLoad = () => {
 | 
				
			||||||
| 
						 | 
					@ -100,7 +107,7 @@ const onImageLoad = () => {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
	cropper = new Cropper(imgEl, {
 | 
						cropper = new Cropper(imgEl!, {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const computedStyle = getComputedStyle(document.documentElement);
 | 
						const computedStyle = getComputedStyle(document.documentElement);
 | 
				
			||||||
| 
						 | 
					@ -112,13 +119,13 @@ onMounted(() => {
 | 
				
			||||||
	selection.outlined = true;
 | 
						selection.outlined = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	window.setTimeout(() => {
 | 
						window.setTimeout(() => {
 | 
				
			||||||
		cropper.getCropperImage()!.$center('contain');
 | 
							cropper!.getCropperImage()!.$center('contain');
 | 
				
			||||||
		selection.$center();
 | 
							selection.$center();
 | 
				
			||||||
	}, 100);
 | 
						}, 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// モーダルオープンアニメーションが終わったあとで再度調整
 | 
						// モーダルオープンアニメーションが終わったあとで再度調整
 | 
				
			||||||
	window.setTimeout(() => {
 | 
						window.setTimeout(() => {
 | 
				
			||||||
		cropper.getCropperImage()!.$center('contain');
 | 
							cropper!.getCropperImage()!.$center('contain');
 | 
				
			||||||
		selection.$center();
 | 
							selection.$center();
 | 
				
			||||||
	}, 500);
 | 
						}, 500);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,6 +44,7 @@ import { getDriveFileMenu } from '@/scripts/get-drive-file-menu';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = withDefaults(defineProps<{
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	file: Misskey.entities.DriveFile;
 | 
						file: Misskey.entities.DriveFile;
 | 
				
			||||||
 | 
						folder: Misskey.entities.DriveFolder | null;
 | 
				
			||||||
	isSelected?: boolean;
 | 
						isSelected?: boolean;
 | 
				
			||||||
	selectMode?: boolean;
 | 
						selectMode?: boolean;
 | 
				
			||||||
}>(), {
 | 
					}>(), {
 | 
				
			||||||
| 
						 | 
					@ -65,12 +66,12 @@ function onClick(ev: MouseEvent) {
 | 
				
			||||||
	if (props.selectMode) {
 | 
						if (props.selectMode) {
 | 
				
			||||||
		emit('chosen', props.file);
 | 
							emit('chosen', props.file);
 | 
				
			||||||
	} else {
 | 
						} 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) {
 | 
					function onContextmenu(ev: MouseEvent) {
 | 
				
			||||||
	os.contextMenu(getDriveFileMenu(props.file), ev);
 | 
						os.contextMenu(getDriveFileMenu(props.file, props.folder), ev);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function onDragstart(ev: DragEvent) {
 | 
					function onDragstart(ev: DragEvent) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,6 +65,7 @@
 | 
				
			||||||
					v-anim="i"
 | 
										v-anim="i"
 | 
				
			||||||
					:class="$style.file"
 | 
										:class="$style.file"
 | 
				
			||||||
					:file="file"
 | 
										:file="file"
 | 
				
			||||||
 | 
										:folder="folder"
 | 
				
			||||||
					:selectMode="select === 'file'"
 | 
										:selectMode="select === 'file'"
 | 
				
			||||||
					:isSelected="selectedFiles.some(x => x.id === file.id)"
 | 
										:isSelected="selectedFiles.some(x => x.id === file.id)"
 | 
				
			||||||
					@chosen="chooseFile"
 | 
										@chosen="chooseFile"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,7 +66,7 @@
 | 
				
			||||||
		<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
 | 
							<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
 | 
						<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"/>
 | 
						<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
 | 
				
			||||||
	<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
 | 
						<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
 | 
				
			||||||
	<div v-if="showingOptions" style="padding: 8px 16px;">
 | 
						<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;
 | 
						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 => {
 | 
						uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
 | 
				
			||||||
		files.push(res);
 | 
							files.push(res);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineAsyncComponent } from 'vue';
 | 
					import { defineAsyncComponent } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 | 
					import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { i18n } from '@/i18n';
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
| 
						 | 
					@ -30,8 +31,9 @@ const props = defineProps<{
 | 
				
			||||||
const emit = defineEmits<{
 | 
					const emit = defineEmits<{
 | 
				
			||||||
	(ev: 'update:modelValue', value: any[]): void;
 | 
						(ev: 'update:modelValue', value: any[]): void;
 | 
				
			||||||
	(ev: 'detach', id: string): void;
 | 
						(ev: 'detach', id: string): void;
 | 
				
			||||||
	(ev: 'changeSensitive'): void;
 | 
						(ev: 'changeSensitive', file: misskey.entities.DriveFile, isSensitive: boolean): void;
 | 
				
			||||||
	(ev: 'changeName'): void;
 | 
						(ev: 'changeName', file: misskey.entities.DriveFile, newName: string): void;
 | 
				
			||||||
 | 
						(ev: 'replaceFile', file: misskey.entities.DriveFile, newFile: misskey.entities.DriveFile): void;
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let menuShowing = false;
 | 
					let menuShowing = false;
 | 
				
			||||||
| 
						 | 
					@ -85,8 +87,15 @@ async function describe(file) {
 | 
				
			||||||
	}, 'closed');
 | 
						}, '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;
 | 
						if (menuShowing) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const isImage = file.type.startsWith('image/');
 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
		text: i18n.ts.renameFile,
 | 
							text: i18n.ts.renameFile,
 | 
				
			||||||
		icon: 'ti ti-forms',
 | 
							icon: 'ti ti-forms',
 | 
				
			||||||
| 
						 | 
					@ -99,7 +108,11 @@ function showFileMenu(file, ev: MouseEvent) {
 | 
				
			||||||
		text: i18n.ts.describeFile,
 | 
							text: i18n.ts.describeFile,
 | 
				
			||||||
		icon: 'ti ti-text-caption',
 | 
							icon: 'ti ti-text-caption',
 | 
				
			||||||
		action: () => { describe(file); },
 | 
							action: () => { describe(file); },
 | 
				
			||||||
	}, {
 | 
						}, ...isImage ? [{
 | 
				
			||||||
 | 
							text: i18n.ts.cropImage,
 | 
				
			||||||
 | 
							icon: 'ti ti-crop',
 | 
				
			||||||
 | 
							action: () : void => { crop(file); },
 | 
				
			||||||
 | 
						}] : [], {
 | 
				
			||||||
		text: i18n.ts.attachCancel,
 | 
							text: i18n.ts.attachCancel,
 | 
				
			||||||
		icon: 'ti ti-circle-x',
 | 
							icon: 'ti ti-circle-x',
 | 
				
			||||||
		action: () => { detachMedia(file.id); },
 | 
							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: {
 | 
					export async function cropImage(image: Misskey.entities.DriveFile, options: {
 | 
				
			||||||
	aspectRatio: number;
 | 
						aspectRatio: number;
 | 
				
			||||||
 | 
						uploadFolder?: string | null;
 | 
				
			||||||
}): Promise<Misskey.entities.DriveFile> {
 | 
					}): Promise<Misskey.entities.DriveFile> {
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
		popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
 | 
							popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
 | 
				
			||||||
			file: image,
 | 
								file: image,
 | 
				
			||||||
			aspectRatio: options.aspectRatio,
 | 
								aspectRatio: options.aspectRatio,
 | 
				
			||||||
 | 
								uploadFolder: options.uploadFolder,
 | 
				
			||||||
		}, {
 | 
							}, {
 | 
				
			||||||
			ok: x => {
 | 
								ok: x => {
 | 
				
			||||||
				resolve(x);
 | 
									resolve(x);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import { defineAsyncComponent } from 'vue';
 | 
				
			||||||
import { i18n } from '@/i18n';
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
					import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					import { MenuItem } from '@/types/menu';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function rename(file: Misskey.entities.DriveFile) {
 | 
					function rename(file: Misskey.entities.DriveFile) {
 | 
				
			||||||
	os.inputText({
 | 
						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 [{
 | 
						return [{
 | 
				
			||||||
		text: i18n.ts.rename,
 | 
							text: i18n.ts.rename,
 | 
				
			||||||
		icon: 'ti ti-forms',
 | 
							icon: 'ti ti-forms',
 | 
				
			||||||
| 
						 | 
					@ -79,7 +81,14 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
 | 
				
			||||||
		text: i18n.ts.describeFile,
 | 
							text: i18n.ts.describeFile,
 | 
				
			||||||
		icon: 'ti ti-text-caption',
 | 
							icon: 'ti ti-text-caption',
 | 
				
			||||||
		action: () => describe(file),
 | 
							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,
 | 
							text: i18n.ts.createNoteFromTheFile,
 | 
				
			||||||
		icon: 'ti ti-pencil',
 | 
							icon: 'ti ti-pencil',
 | 
				
			||||||
		action: () => os.post({
 | 
							action: () => os.post({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue