From bf57557ba3566689bef97134f3c637aa43dd3605 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:10:15 +0900 Subject: [PATCH] refactor(frontend): refactor uploader image editing features and menu Replaces separate 'effect' and 'crop' features with a unified 'imageEditing' feature in the uploader. Groups crop and effect actions under a new parent 'editImage' menu item, adds localization for 'editImage', and updates supported types accordingly. --- locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + .../frontend/src/composables/use-uploader.ts | 112 ++++++++---------- 3 files changed, 56 insertions(+), 61 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 2ba4d7b0e4..52b333b689 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11991,6 +11991,10 @@ export interface Locale extends ILocale { }; }; "_uploader": { + /** + * 画像の編集 + */ + "editImage": string; /** * {x}に圧縮 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 98b00c8435..dea1c4e4fa 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3208,6 +3208,7 @@ _serverSetupWizard: text3: "支援者向け特典もあります!" _uploader: + editImage: "画像の編集" compressedToX: "{x}に圧縮" savedXPercent: "{x}%節約" abortConfirm: "アップロードされていないファイルがありますが、中止しますか?" diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index ec01ed1a3c..6f4ed81f82 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -19,9 +19,8 @@ import { ensureSignin } from '@/i.js'; import { WatermarkRenderer } from '@/utility/watermark.js'; export type UploaderFeatures = { - effect?: boolean; + imageEditing?: boolean; watermark?: boolean; - crop?: boolean; }; const THUMBNAIL_SUPPORTED_TYPES = [ @@ -38,12 +37,6 @@ const IMAGE_COMPRESSION_SUPPORTED_TYPES = [ 'image/svg+xml', ]; -const CROPPING_SUPPORTED_TYPES = [ - 'image/jpeg', - 'image/png', - 'image/webp', -]; - const IMAGE_EDITING_SUPPORTED_TYPES = [ 'image/jpeg', 'image/png', @@ -55,7 +48,6 @@ const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES; const IMAGE_PREPROCESS_NEEDED_TYPES = [ ...WATERMARK_SUPPORTED_TYPES, ...IMAGE_COMPRESSION_SUPPORTED_TYPES, - ...CROPPING_SUPPORTED_TYPES, ...IMAGE_EDITING_SUPPORTED_TYPES, ]; @@ -112,17 +104,14 @@ export function useUploader(options: { multiple?: boolean; features?: UploaderFeatures; } = {}) { - const $i = ensureSignin(); - const events = new EventEmitter<{ 'itemUploaded': (ctx: { item: UploaderItem; }) => void; }>(); const uploaderFeatures = computed>(() => { return { - effect: options.features?.effect ?? true, + imageEditing: options.features?.imageEditing ?? true, watermark: options.features?.watermark ?? true, - crop: options.features?.crop ?? true, }; }); @@ -215,60 +204,61 @@ export function useUploader(options: { } if ( - uploaderFeatures.value.crop && - CROPPING_SUPPORTED_TYPES.includes(item.file.type) && - !item.preprocessing && - !item.uploading && - !item.uploaded - ) { - menu.push({ - icon: 'ti ti-crop', - text: i18n.ts.cropImage, - action: async () => { - const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); - if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - items.value.splice(items.value.indexOf(item), 1, { - ...item, - file: markRaw(cropped), - thumbnail: window.URL.createObjectURL(cropped), - }); - const reactiveItem = items.value.find(x => x.id === item.id)!; - preprocess(reactiveItem).then(() => { - triggerRef(items); - }); - }, - }); - } - - if ( - uploaderFeatures.value.effect && + uploaderFeatures.value.imageEditing && IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded ) { menu.push({ - icon: 'ti ti-sparkles', - text: i18n.ts._imageEffector.title + ' (BETA)', - action: async () => { - const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), { - image: item.file, - }, { - ok: (file) => { - if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - items.value.splice(items.value.indexOf(item), 1, { - ...item, - file: markRaw(file), - thumbnail: window.URL.createObjectURL(file), - }); - const reactiveItem = items.value.find(x => x.id === item.id)!; - preprocess(reactiveItem).then(() => { - triggerRef(items); - }); - }, - closed: () => dispose(), - }); - }, + type: 'parent', + icon: 'ti ti-photo-edit', + text: i18n.ts._uploader.editImage, + children: [{ + icon: 'ti ti-crop', + text: i18n.ts.cropImage, + action: async () => { + const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); + if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); + items.value.splice(items.value.indexOf(item), 1, { + ...item, + file: markRaw(cropped), + thumbnail: window.URL.createObjectURL(cropped), + }); + const reactiveItem = items.value.find(x => x.id === item.id)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); + }); + }, + }, /*{ + icon: 'ti ti-resize', + text: i18n.ts.resize, + action: async () => { + // TODO + }, + },*/ { + icon: 'ti ti-sparkles', + text: i18n.ts._imageEffector.title + ' (BETA)', + action: async () => { + const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), { + image: item.file, + }, { + ok: (file) => { + if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); + items.value.splice(items.value.indexOf(item), 1, { + ...item, + file: markRaw(file), + thumbnail: window.URL.createObjectURL(file), + }); + const reactiveItem = items.value.find(x => x.id === item.id)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); + }); + }, + closed: () => dispose(), + }); + }, + }], }); }