diff --git a/packages/frontend/src/components/MkImageFrameEditorDialog.vue b/packages/frontend/src/components/MkImageFrameEditorDialog.vue index e13ec3a4e0..f450899f99 100644 --- a/packages/frontend/src/components/MkImageFrameEditorDialog.vue +++ b/packages/frontend/src/components/MkImageFrameEditorDialog.vue @@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + - + @@ -47,31 +47,31 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + - + - + - + - + - +
@@ -81,31 +81,31 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + - + - + - + - + - +
@@ -132,7 +132,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue'; import ExifReader from 'exifreader'; import { throttle } from 'throttle-debounce'; -import type { ImageFrameParams } from '@/utility/image-frame-renderer/image-frame-renderer.js'; +import type { ImageFrameParams, ImageFramePreset } from '@/utility/image-frame-renderer/image-frame-renderer.js'; import { ImageFrameRenderer } from '@/utility/image-frame-renderer/image-frame-renderer.js'; import { i18n } from '@/i18n.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; @@ -153,22 +153,19 @@ import { useMkSelect } from '@/composables/use-mkselect.js'; const $i = ensureSignin(); -const EXIF_MOCK = { - DateTimeOriginal: { description: '2012:03:04 5:06:07' }, - Model: { description: 'Example camera' }, - LensModel: { description: 'Example lens 123mm f/1.23' }, - FocalLength: { description: '123mm' }, - ExposureTime: { description: '1/234' }, - FNumber: { description: '1.23' }, - ISOSpeedRatings: { description: '123' }, -} satisfies ExifReader.Tags; - const props = defineProps<{ - frame?: ImageFrameParams | null; + presetEditMode?: boolean; + preset?: ImageFramePreset | null; + params?: ImageFrameParams | null; image?: File | null; }>(); -const frame = reactive(deepClone(props.frame) ?? { +const preset = deepClone(props.preset) ?? { + id: genId(), + name: '', +}; + +const params = reactive(deepClone(props.params) ?? { borderThickness: 0.05, labelTop: { enabled: false, @@ -194,6 +191,7 @@ const frame = reactive(deepClone(props.frame) ?? { const emit = defineEmits<{ (ev: 'ok', frame: ImageFrameParams): void; + (ev: 'presetOk', preset: ImageFramePreset): void; (ev: 'cancel'): void; (ev: 'closed'): void; }>(); @@ -206,11 +204,11 @@ async function cancel() { const updateThrottled = throttle(50, () => { if (renderer != null) { - renderer.render(frame); + renderer.render(params); } }); -watch(frame, async (newValue, oldValue) => { +watch(params, async (newValue, oldValue) => { updateThrottled(); }, { deep: true }); @@ -262,14 +260,14 @@ async function initRenderer() { renderer = new ImageFrameRenderer({ canvas: canvasEl.value, image: sampleImage_3_2, - exif: EXIF_MOCK, + exif: null, renderAsPreview: true, }); } else if (sampleImageType.value === '2_3') { renderer = new ImageFrameRenderer({ canvas: canvasEl.value, image: sampleImage_2_3, - exif: EXIF_MOCK, + exif: null, renderAsPreview: true, }); } else if (imageFile != null) { @@ -285,7 +283,7 @@ async function initRenderer() { }); } - await renderer!.render(frame); + await renderer!.render(params); } onMounted(async () => { @@ -313,21 +311,34 @@ onUnmounted(() => { }); async function save() { - const { canceled, result: name } = await os.inputText({ - title: i18n.ts.name, - default: preset.name, - }); - if (canceled) return; + if (props.presetEditMode) { + const { canceled, result: name } = await os.inputText({ + title: i18n.ts.name, + default: preset.name, + }); + if (canceled) return; - preset.name = name || ''; + preset.name = name || ''; - dialog.value?.close(); - if (renderer != null) { - renderer.destroy(); - renderer = null; + dialog.value?.close(); + if (renderer != null) { + renderer.destroy(); + renderer = null; + } + + emit('presetOk', { + ...preset, + params: deepClone(params), + }); + } else { + dialog.value?.close(); + if (renderer != null) { + renderer.destroy(); + renderer = null; + } + + emit('ok', params); } - - emit('ok', preset); } function getHex(c: [number, number, number]) { diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index 6e7019f7d5..1c926d995b 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -343,8 +343,8 @@ export function useUploader(options: { !item.uploading && !item.uploaded ) { - function changePreset(preset: ImageFramePreset | null) { - item.imageFramePreset = preset; + function change(params: ImageFrameParams | null) { + item.imageFrameParams = params; preprocess(item).then(() => { triggerRef(items); }); @@ -355,35 +355,41 @@ export function useUploader(options: { text: i18n.ts.frame, type: 'parent', children: [{ - type: 'radioOption', - text: i18n.ts.none, - active: computed(() => item.imageFrameParams == null), - action: () => changePreset(null), - }, { - type: 'divider', - }, ...prefer.s.imageFramePresets.map(preset => ({ - type: 'radioOption' as const, - text: preset.name, - active: computed(() => item.imageFramePreset?.id === preset.id), - action: () => changePreset(preset), - })), ...(prefer.s.imageFramePresets.length > 0 ? [{ - type: 'divider' as const, - }] : []), { type: 'button', - icon: 'ti ti-plus', - text: i18n.ts.add, + icon: 'ti ti-pencil', + text: i18n.ts.edit, action: async () => { const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), { + params: item.imageFrameParams, image: item.file, }, { - ok: (preset) => { - prefer.commit('imageFramePresets', [...prefer.s.imageFramePresets, preset]); - changePreset(preset.id); + ok: (params) => { + change(params); }, closed: () => dispose(), }); }, - }], + }, { + type: 'button', + text: i18n.ts.remove, + action: () => change(null), + }, { + type: 'divider', + }, ...prefer.s.imageFramePresets.map(preset => ({ + type: 'button' as const, + text: preset.name, + action: async () => { + const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), { + params: preset.params, + image: item.file, + }, { + ok: (params) => { + change(params); + }, + closed: () => dispose(), + }); + }, + }))], }); } @@ -625,33 +631,28 @@ export function useUploader(options: { }); } - const canvas = window.document.createElement('canvas'); + const needsImageFrame = item.imageFrameParams != null && IMAGE_EDITING_SUPPORTED_TYPES.includes(preprocessedFile.type); + if (needsImageFrame && item.imageFrameParams != null) { + const canvas = window.document.createElement('canvas'); + const exif = await ExifReader.load(await item.file.arrayBuffer()); + const frameRenderer = new ImageFrameRenderer({ + canvas: canvas, + image: await window.createImageBitmap(preprocessedFile), + exif, + }); - const exif = await ExifReader.load(await item.file.arrayBuffer()); + await frameRenderer.render(item.imageFrameParams); - const frameRenderer = new ImageFrameRenderer({ - canvas: canvas, - image: await window.createImageBitmap(preprocessedFile), - exif, - }); - //await frameRenderer.update({ - // title: `${meta_model} + ${meta_lensModel}`, - // text: `${date} ${meta_mm}mm f/${meta_f} ${meta_s}s ISO${meta_iso}`, - //}); - await frameRenderer.render({ - title: 'aaaaaaaaaaaaa', - text: 'bbbbbbbbbbbbbbbbbbbb', - }); - - preprocessedFile = await new Promise((resolve) => { - canvas.toBlob((blob) => { - if (blob == null) { - throw new Error('Failed to convert canvas to blob'); - } - resolve(blob); - frameRenderer.destroy(); - }, 'image/png'); - }); + preprocessedFile = await new Promise((resolve) => { + canvas.toBlob((blob) => { + if (blob == null) { + throw new Error('Failed to convert canvas to blob'); + } + resolve(blob); + frameRenderer.destroy(); + }, 'image/png'); + }); + } const compressionSettings = getCompressionSettings(item.compressionLevel); const needsCompress = item.compressionLevel !== 0 && compressionSettings && IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type) && !(await isAnimated(preprocessedFile)); diff --git a/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue b/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue new file mode 100644 index 0000000000..3b333f8d47 --- /dev/null +++ b/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue @@ -0,0 +1,111 @@ + + + + + + + diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 41953052a6..947f15000f 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -132,38 +132,22 @@ SPDX-License-Identifier: AGPL-3.0-only
- -
- -
- -
@@ -219,7 +203,9 @@ import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import tinycolor from 'tinycolor2'; import XWatermarkItem from './drive.WatermarkItem.vue'; +import XImageFrameItem from './drive.ImageFrameItem.vue'; import type { WatermarkPreset } from '@/utility/watermark.js'; +import type { ImageFramePreset } from '@/utility/image-frame-renderer/image-frame-renderer.js'; import FormLink from '@/components/form/link.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -239,6 +225,7 @@ import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; import { selectDriveFolder } from '@/utility/drive.js'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; +import { genId } from '@/utility/id.js'; const $i = ensureSignin(); @@ -280,6 +267,20 @@ function changeWatermarkPresetsSyncEnabled(value: boolean) { } } +const imageFramePresetsSyncEnabled = ref(prefer.isSyncEnabled('imageFramePresets')); + +function changeImageFramePresetsSyncEnabled(value: boolean) { + if (value) { + prefer.enableSync('imageFramePresets').then((res) => { + if (res == null) return; + if (res.enabled) imageFramePresetsSyncEnabled.value = true; + }); + } else { + prefer.disableSync('imageFramePresets'); + imageFramePresetsSyncEnabled.value = false; + } +} + misskeyApi('drive').then(info => { capacity.value = info.capacity; usage.value = info.usage; @@ -343,11 +344,35 @@ function onDeleteWatermarkPreset(id: string) { } } +function onUpdateImageFramePreset(id: string, preset: ImageFramePreset) { + const index = prefer.s.imageFramePresets.findIndex(p => p.id === id); + if (index !== -1) { + prefer.commit('imageFramePresets', [ + ...prefer.s.imageFramePresets.slice(0, index), + preset, + ...prefer.s.imageFramePresets.slice(index + 1), + ]); + } +} + +function onDeleteImageFramePreset(id: string) { + const index = prefer.s.imageFramePresets.findIndex(p => p.id === id); + if (index !== -1) { + prefer.commit('imageFramePresets', [ + ...prefer.s.imageFramePresets.slice(0, index), + ...prefer.s.imageFramePresets.slice(index + 1), + ]); + } +} + async function addImageFramePreset() { const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), { + presetEditMode: true, + preset: null, + params: null, }, { - ok: (preset: any) => { - //prefer.commit('imageFramePresets', [...prefer.s.imageFramePresets, preset]); + presetOk: (preset) => { + prefer.commit('imageFramePresets', [...prefer.s.imageFramePresets, preset]); }, closed: () => dispose(), }); diff --git a/packages/frontend/src/utility/image-frame-renderer/image-frame-renderer.ts b/packages/frontend/src/utility/image-frame-renderer/image-frame-renderer.ts index fcf5c070b5..608f8efe78 100644 --- a/packages/frontend/src/utility/image-frame-renderer/image-frame-renderer.ts +++ b/packages/frontend/src/utility/image-frame-renderer/image-frame-renderer.ts @@ -38,6 +38,16 @@ export type ImageFramePreset = { params: ImageFrameParams; }; +const EXIF_MOCK = { + DateTimeOriginal: { description: '2012:03:04 5:06:07' }, + Model: { description: 'Example camera' }, + LensModel: { description: 'Example lens 123mm f/1.23' }, + FocalLength: { description: '123mm' }, + ExposureTime: { description: '1/234' }, + FNumber: { description: '1.23' }, + ISOSpeedRatings: { description: '123' }, +} satisfies ExifReader.Tags; + export class ImageFrameRenderer { private compositor: ImageCompositor; private image: HTMLImageElement | ImageBitmap; @@ -47,11 +57,11 @@ export class ImageFrameRenderer { constructor(options: { canvas: HTMLCanvasElement, image: HTMLImageElement | ImageBitmap, - exif: ExifReader.Tags, + exif: ExifReader.Tags | null, renderAsPreview?: boolean, }) { this.image = options.image; - this.exif = options.exif; + this.exif = options.exif ?? EXIF_MOCK; this.renderAsPreview = options.renderAsPreview ?? false; console.log(this.exif);