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.
This commit is contained in:
syuilo
2025-06-26 12:10:15 +09:00
parent 8fda4fefaf
commit bf57557ba3
3 changed files with 56 additions and 61 deletions

4
locales/index.d.ts vendored
View File

@@ -11991,6 +11991,10 @@ export interface Locale extends ILocale {
}; };
}; };
"_uploader": { "_uploader": {
/**
* 画像の編集
*/
"editImage": string;
/** /**
* {x}に圧縮 * {x}に圧縮
*/ */

View File

@@ -3208,6 +3208,7 @@ _serverSetupWizard:
text3: "支援者向け特典もあります!" text3: "支援者向け特典もあります!"
_uploader: _uploader:
editImage: "画像の編集"
compressedToX: "{x}に圧縮" compressedToX: "{x}に圧縮"
savedXPercent: "{x}%節約" savedXPercent: "{x}%節約"
abortConfirm: "アップロードされていないファイルがありますが、中止しますか?" abortConfirm: "アップロードされていないファイルがありますが、中止しますか?"

View File

@@ -19,9 +19,8 @@ import { ensureSignin } from '@/i.js';
import { WatermarkRenderer } from '@/utility/watermark.js'; import { WatermarkRenderer } from '@/utility/watermark.js';
export type UploaderFeatures = { export type UploaderFeatures = {
effect?: boolean; imageEditing?: boolean;
watermark?: boolean; watermark?: boolean;
crop?: boolean;
}; };
const THUMBNAIL_SUPPORTED_TYPES = [ const THUMBNAIL_SUPPORTED_TYPES = [
@@ -38,12 +37,6 @@ const IMAGE_COMPRESSION_SUPPORTED_TYPES = [
'image/svg+xml', 'image/svg+xml',
]; ];
const CROPPING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const IMAGE_EDITING_SUPPORTED_TYPES = [ const IMAGE_EDITING_SUPPORTED_TYPES = [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@@ -55,7 +48,6 @@ const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
const IMAGE_PREPROCESS_NEEDED_TYPES = [ const IMAGE_PREPROCESS_NEEDED_TYPES = [
...WATERMARK_SUPPORTED_TYPES, ...WATERMARK_SUPPORTED_TYPES,
...IMAGE_COMPRESSION_SUPPORTED_TYPES, ...IMAGE_COMPRESSION_SUPPORTED_TYPES,
...CROPPING_SUPPORTED_TYPES,
...IMAGE_EDITING_SUPPORTED_TYPES, ...IMAGE_EDITING_SUPPORTED_TYPES,
]; ];
@@ -112,17 +104,14 @@ export function useUploader(options: {
multiple?: boolean; multiple?: boolean;
features?: UploaderFeatures; features?: UploaderFeatures;
} = {}) { } = {}) {
const $i = ensureSignin();
const events = new EventEmitter<{ const events = new EventEmitter<{
'itemUploaded': (ctx: { item: UploaderItem; }) => void; 'itemUploaded': (ctx: { item: UploaderItem; }) => void;
}>(); }>();
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => { const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
return { return {
effect: options.features?.effect ?? true, imageEditing: options.features?.imageEditing ?? true,
watermark: options.features?.watermark ?? true, watermark: options.features?.watermark ?? true,
crop: options.features?.crop ?? true,
}; };
}); });
@@ -215,60 +204,61 @@ export function useUploader(options: {
} }
if ( if (
uploaderFeatures.value.crop && uploaderFeatures.value.imageEditing &&
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 &&
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing && !item.preprocessing &&
!item.uploading && !item.uploading &&
!item.uploaded !item.uploaded
) { ) {
menu.push({ menu.push({
icon: 'ti ti-sparkles', type: 'parent',
text: i18n.ts._imageEffector.title + ' (BETA)', icon: 'ti ti-photo-edit',
action: async () => { text: i18n.ts._uploader.editImage,
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), { children: [{
image: item.file, icon: 'ti ti-crop',
}, { text: i18n.ts.cropImage,
ok: (file) => { action: async () => {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
items.value.splice(items.value.indexOf(item), 1, { if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
...item, items.value.splice(items.value.indexOf(item), 1, {
file: markRaw(file), ...item,
thumbnail: window.URL.createObjectURL(file), file: markRaw(cropped),
}); thumbnail: window.URL.createObjectURL(cropped),
const reactiveItem = items.value.find(x => x.id === item.id)!; });
preprocess(reactiveItem).then(() => { const reactiveItem = items.value.find(x => x.id === item.id)!;
triggerRef(items); preprocess(reactiveItem).then(() => {
}); triggerRef(items);
}, });
closed: () => dispose(), },
}); }, /*{
}, 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(),
});
},
}],
}); });
} }