From 6becc489dc5e9d7e54dbd83c25500186164aae91 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 3 Nov 2025 20:31:37 +0900
Subject: [PATCH] wip
---
.../components/MkImageFrameEditorDialog.vue | 105 +++++++++--------
.../frontend/src/composables/use-uploader.ts | 95 +++++++--------
.../pages/settings/drive.ImageFrameItem.vue | 111 ++++++++++++++++++
.../frontend/src/pages/settings/drive.vue | 73 ++++++++----
.../image-frame-renderer.ts | 14 ++-
5 files changed, 278 insertions(+), 120 deletions(-)
create mode 100644 packages/frontend/src/pages/settings/drive.ImageFrameItem.vue
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
-
+
{{ i18n.ts._imageFrameEditor.borderThickness }}
- { const c = getRgb(v); if (c != null) frame.bgColor = c; }">
+ { const c = getRgb(v); if (c != null) params.bgColor = c; }">
{{ i18n.ts._imageFrameEditor.backgroundColor }}
- { const c = getRgb(v); if (c != null) frame.fgColor = c; }">
+ { const c = getRgb(v); if (c != null) params.fgColor = c; }">
{{ i18n.ts._imageFrameEditor.textColor }}
@@ -47,31 +47,31 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._imageFrameEditor.header }}
-
+
{{ i18n.ts.show }}
-
+
{{ i18n.ts._imageFrameEditor.labelThickness }}
-
+
{{ i18n.ts._imageFrameEditor.labelScale }}
-
+
{{ i18n.ts._imageFrameEditor.centered }}
-
+
{{ i18n.ts._imageFrameEditor.captionMain }}
-
+
{{ i18n.ts._imageFrameEditor.captionSub }}
-
+
{{ i18n.ts._imageFrameEditor.withQrCode }}
@@ -81,31 +81,31 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._imageFrameEditor.footer }}
-
+
{{ i18n.ts.show }}
-
+
{{ i18n.ts._imageFrameEditor.labelThickness }}
-
+
{{ i18n.ts._imageFrameEditor.labelScale }}
-
+
{{ i18n.ts._imageFrameEditor.centered }}
-
+
{{ i18n.ts._imageFrameEditor.captionMain }}
-
+
{{ i18n.ts._imageFrameEditor.captionSub }}
-
+
{{ i18n.ts._imageFrameEditor.withQrCode }}
@@ -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 @@
+
+
+
+
+
+ {{ i18n.ts.preset }}: {{ preset.name === '' ? '(' + i18n.ts.noName + ')' : preset.name }}
+
+
+ {{ i18n.ts.edit }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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);