wip
This commit is contained in:
parent
e4b6853f0a
commit
6becc489dc
|
|
@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.controls">
|
<div :class="$style.controls">
|
||||||
<div class="_spacer _gaps">
|
<div class="_spacer _gaps">
|
||||||
<MkRange v-model="frame.borderThickness" :min="0" :max="0.2" :step="0.01" :continuousUpdate="true">
|
<MkRange v-model="params.borderThickness" :min="0" :max="0.2" :step="0.01" :continuousUpdate="true">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.borderThickness }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.borderThickness }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
|
|
||||||
<MkInput :modelValue="getHex(frame.bgColor)" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) frame.bgColor = c; }">
|
<MkInput :modelValue="getHex(params.bgColor)" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) params.bgColor = c; }">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.backgroundColor }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.backgroundColor }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput :modelValue="getHex(frame.fgColor)" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) frame.fgColor = c; }">
|
<MkInput :modelValue="getHex(params.fgColor)" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) params.fgColor = c; }">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.textColor }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.textColor }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
|
@ -47,31 +47,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.header }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.header }}</template>
|
||||||
|
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkSwitch v-model="frame.labelTop.enabled">
|
<MkSwitch v-model="params.labelTop.enabled">
|
||||||
<template #label>{{ i18n.ts.show }}</template>
|
<template #label>{{ i18n.ts.show }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
<MkRange v-model="frame.labelTop.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
|
<MkRange v-model="params.labelTop.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
|
|
||||||
<MkRange v-model="frame.labelTop.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
|
<MkRange v-model="params.labelTop.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
|
|
||||||
<MkSwitch v-model="frame.labelTop.centered">
|
<MkSwitch v-model="params.labelTop.centered">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
<MkInput v-model="frame.labelTop.textBig">
|
<MkInput v-model="params.labelTop.textBig">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkTextarea v-model="frame.labelTop.textSmall">
|
<MkTextarea v-model="params.labelTop.textSmall">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
<MkSwitch v-model="frame.labelTop.withQrCode">
|
<MkSwitch v-model="params.labelTop.withQrCode">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,31 +81,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.footer }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.footer }}</template>
|
||||||
|
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkSwitch v-model="frame.labelBottom.enabled">
|
<MkSwitch v-model="params.labelBottom.enabled">
|
||||||
<template #label>{{ i18n.ts.show }}</template>
|
<template #label>{{ i18n.ts.show }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
<MkRange v-model="frame.labelBottom.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
|
<MkRange v-model="params.labelBottom.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
|
|
||||||
<MkRange v-model="frame.labelBottom.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
|
<MkRange v-model="params.labelBottom.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
|
|
||||||
<MkSwitch v-model="frame.labelBottom.centered">
|
<MkSwitch v-model="params.labelBottom.centered">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
<MkInput v-model="frame.labelBottom.textBig">
|
<MkInput v-model="params.labelBottom.textBig">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkTextarea v-model="frame.labelBottom.textSmall">
|
<MkTextarea v-model="params.labelBottom.textSmall">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
<MkSwitch v-model="frame.labelBottom.withQrCode">
|
<MkSwitch v-model="params.labelBottom.withQrCode">
|
||||||
<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
|
<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -132,7 +132,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
|
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
|
||||||
import ExifReader from 'exifreader';
|
import ExifReader from 'exifreader';
|
||||||
import { throttle } from 'throttle-debounce';
|
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 { ImageFrameRenderer } from '@/utility/image-frame-renderer/image-frame-renderer.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
|
@ -153,22 +153,19 @@ import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
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<{
|
const props = defineProps<{
|
||||||
frame?: ImageFrameParams | null;
|
presetEditMode?: boolean;
|
||||||
|
preset?: ImageFramePreset | null;
|
||||||
|
params?: ImageFrameParams | null;
|
||||||
image?: File | null;
|
image?: File | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const frame = reactive<ImageFrameParams>(deepClone(props.frame) ?? {
|
const preset = deepClone(props.preset) ?? {
|
||||||
|
id: genId(),
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = reactive<ImageFrameParams>(deepClone(props.params) ?? {
|
||||||
borderThickness: 0.05,
|
borderThickness: 0.05,
|
||||||
labelTop: {
|
labelTop: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
@ -194,6 +191,7 @@ const frame = reactive<ImageFrameParams>(deepClone(props.frame) ?? {
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'ok', frame: ImageFrameParams): void;
|
(ev: 'ok', frame: ImageFrameParams): void;
|
||||||
|
(ev: 'presetOk', preset: ImageFramePreset): void;
|
||||||
(ev: 'cancel'): void;
|
(ev: 'cancel'): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -206,11 +204,11 @@ async function cancel() {
|
||||||
|
|
||||||
const updateThrottled = throttle(50, () => {
|
const updateThrottled = throttle(50, () => {
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
renderer.render(frame);
|
renderer.render(params);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(frame, async (newValue, oldValue) => {
|
watch(params, async (newValue, oldValue) => {
|
||||||
updateThrottled();
|
updateThrottled();
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
|
|
@ -262,14 +260,14 @@ async function initRenderer() {
|
||||||
renderer = new ImageFrameRenderer({
|
renderer = new ImageFrameRenderer({
|
||||||
canvas: canvasEl.value,
|
canvas: canvasEl.value,
|
||||||
image: sampleImage_3_2,
|
image: sampleImage_3_2,
|
||||||
exif: EXIF_MOCK,
|
exif: null,
|
||||||
renderAsPreview: true,
|
renderAsPreview: true,
|
||||||
});
|
});
|
||||||
} else if (sampleImageType.value === '2_3') {
|
} else if (sampleImageType.value === '2_3') {
|
||||||
renderer = new ImageFrameRenderer({
|
renderer = new ImageFrameRenderer({
|
||||||
canvas: canvasEl.value,
|
canvas: canvasEl.value,
|
||||||
image: sampleImage_2_3,
|
image: sampleImage_2_3,
|
||||||
exif: EXIF_MOCK,
|
exif: null,
|
||||||
renderAsPreview: true,
|
renderAsPreview: true,
|
||||||
});
|
});
|
||||||
} else if (imageFile != null) {
|
} else if (imageFile != null) {
|
||||||
|
|
@ -285,7 +283,7 @@ async function initRenderer() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await renderer!.render(frame);
|
await renderer!.render(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
@ -313,6 +311,7 @@ onUnmounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (props.presetEditMode) {
|
||||||
const { canceled, result: name } = await os.inputText({
|
const { canceled, result: name } = await os.inputText({
|
||||||
title: i18n.ts.name,
|
title: i18n.ts.name,
|
||||||
default: preset.name,
|
default: preset.name,
|
||||||
|
|
@ -327,7 +326,19 @@ async function save() {
|
||||||
renderer = null;
|
renderer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('ok', preset);
|
emit('presetOk', {
|
||||||
|
...preset,
|
||||||
|
params: deepClone(params),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dialog.value?.close();
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.destroy();
|
||||||
|
renderer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('ok', params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHex(c: [number, number, number]) {
|
function getHex(c: [number, number, number]) {
|
||||||
|
|
|
||||||
|
|
@ -343,8 +343,8 @@ export function useUploader(options: {
|
||||||
!item.uploading &&
|
!item.uploading &&
|
||||||
!item.uploaded
|
!item.uploaded
|
||||||
) {
|
) {
|
||||||
function changePreset(preset: ImageFramePreset | null) {
|
function change(params: ImageFrameParams | null) {
|
||||||
item.imageFramePreset = preset;
|
item.imageFrameParams = params;
|
||||||
preprocess(item).then(() => {
|
preprocess(item).then(() => {
|
||||||
triggerRef(items);
|
triggerRef(items);
|
||||||
});
|
});
|
||||||
|
|
@ -355,35 +355,41 @@ export function useUploader(options: {
|
||||||
text: i18n.ts.frame,
|
text: i18n.ts.frame,
|
||||||
type: 'parent',
|
type: 'parent',
|
||||||
children: [{
|
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',
|
type: 'button',
|
||||||
icon: 'ti ti-plus',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.add,
|
text: i18n.ts.edit,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), {
|
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), {
|
||||||
|
params: item.imageFrameParams,
|
||||||
image: item.file,
|
image: item.file,
|
||||||
}, {
|
}, {
|
||||||
ok: (preset) => {
|
ok: (params) => {
|
||||||
prefer.commit('imageFramePresets', [...prefer.s.imageFramePresets, preset]);
|
change(params);
|
||||||
changePreset(preset.id);
|
|
||||||
},
|
},
|
||||||
closed: () => dispose(),
|
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,23 +631,17 @@ export function useUploader(options: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const needsImageFrame = item.imageFrameParams != null && IMAGE_EDITING_SUPPORTED_TYPES.includes(preprocessedFile.type);
|
||||||
|
if (needsImageFrame && item.imageFrameParams != null) {
|
||||||
const canvas = window.document.createElement('canvas');
|
const canvas = window.document.createElement('canvas');
|
||||||
|
|
||||||
const exif = await ExifReader.load(await item.file.arrayBuffer());
|
const exif = await ExifReader.load(await item.file.arrayBuffer());
|
||||||
|
|
||||||
const frameRenderer = new ImageFrameRenderer({
|
const frameRenderer = new ImageFrameRenderer({
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
image: await window.createImageBitmap(preprocessedFile),
|
image: await window.createImageBitmap(preprocessedFile),
|
||||||
exif,
|
exif,
|
||||||
});
|
});
|
||||||
//await frameRenderer.update({
|
|
||||||
// title: `${meta_model} + ${meta_lensModel}`,
|
await frameRenderer.render(item.imageFrameParams);
|
||||||
// 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<Blob>((resolve) => {
|
preprocessedFile = await new Promise<Blob>((resolve) => {
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
|
|
@ -652,6 +652,7 @@ export function useUploader(options: {
|
||||||
frameRenderer.destroy();
|
frameRenderer.destroy();
|
||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const compressionSettings = getCompressionSettings(item.compressionLevel);
|
const compressionSettings = getCompressionSettings(item.compressionLevel);
|
||||||
const needsCompress = item.compressionLevel !== 0 && compressionSettings && IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type) && !(await isAnimated(preprocessedFile));
|
const needsCompress = item.compressionLevel !== 0 && compressionSettings && IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type) && !(await isAnimated(preprocessedFile));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkFolder :defaultOpen="false" :canPage="false">
|
||||||
|
<template #icon><i class="ti ti-pencil"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.preset }}: {{ preset.name === '' ? '(' + i18n.ts.noName + ')' : preset.name }}</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton @click="edit"><i class="ti ti-pencil"></i> {{ i18n.ts.edit }}</MkButton>
|
||||||
|
<MkButton danger iconOnly style="margin-left: auto;" @click="del"><i class="ti ti-trash"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
|
import type { ImageFramePreset } from '@/utility/image-frame-renderer/image-frame-renderer.js';
|
||||||
|
import { ImageFrameRenderer } from '@/utility/image-frame-renderer/image-frame-renderer.js';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
preset: ImageFramePreset;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'updatePreset', preset: ImageFramePreset): void,
|
||||||
|
(ev: 'del'): void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
async function edit() {
|
||||||
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkImageFrameEditorDialog.vue')), {
|
||||||
|
presetEditMode: true,
|
||||||
|
preset: deepClone(props.preset),
|
||||||
|
params: deepClone(props.preset.params),
|
||||||
|
}, {
|
||||||
|
presetOk: (preset) => {
|
||||||
|
emit('updatePreset', preset);
|
||||||
|
},
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: i18n.ts.delete,
|
||||||
|
action: () => {
|
||||||
|
emit('del');
|
||||||
|
},
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasEl = useTemplateRef('canvasEl');
|
||||||
|
|
||||||
|
const sampleImage = new Image();
|
||||||
|
sampleImage.src = '/client-assets/sample/3-2.jpg';
|
||||||
|
|
||||||
|
let renderer: ImageFrameRenderer | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
sampleImage.onload = async () => {
|
||||||
|
watch(canvasEl, async () => {
|
||||||
|
if (canvasEl.value == null) return;
|
||||||
|
|
||||||
|
renderer = new ImageFrameRenderer({
|
||||||
|
canvas: canvasEl.value,
|
||||||
|
image: sampleImage,
|
||||||
|
exif: null,
|
||||||
|
renderAsPreview: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderer.render(props.preset.params);
|
||||||
|
}, { immediate: true });
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.destroy();
|
||||||
|
renderer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.preset, async () => {
|
||||||
|
if (renderer != null) {
|
||||||
|
await renderer.render(props.preset.params);
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.previewCanvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -132,38 +132,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<!--
|
<XImageFrameItem
|
||||||
<XWatermarkItem
|
v-for="(preset, i) in prefer.r.imageFramePresets.value"
|
||||||
v-for="(preset, i) in prefer.r.watermarkPresets.value"
|
|
||||||
:key="preset.id"
|
:key="preset.id"
|
||||||
:preset="preset"
|
:preset="preset"
|
||||||
@updatePreset="onUpdateWatermarkPreset(preset.id, $event)"
|
@updatePreset="onUpdateImageFramePreset(preset.id, $event)"
|
||||||
@del="onDeleteWatermarkPreset(preset.id)"
|
@del="onDeleteImageFramePreset(preset.id)"
|
||||||
/>
|
/>
|
||||||
-->
|
|
||||||
|
|
||||||
<MkButton iconOnly rounded style="margin: 0 auto;" @click="addImageFramePreset"><i class="ti ti-plus"></i></MkButton>
|
<MkButton iconOnly rounded style="margin: 0 auto;" @click="addImageFramePreset"><i class="ti ti-plus"></i></MkButton>
|
||||||
|
|
||||||
<!--
|
<SearchMarker :keywords="['sync', 'frame', 'label', 'preset', 'devices']">
|
||||||
<SearchMarker :keywords="['sync', 'watermark', 'preset', 'devices']">
|
<MkSwitch :modelValue="imageFramePresetsSyncEnabled" @update:modelValue="changeImageFramePresetsSyncEnabled">
|
||||||
<MkSwitch :modelValue="watermarkPresetsSyncEnabled" @update:modelValue="changeWatermarkPresetsSyncEnabled">
|
|
||||||
<template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts.syncBetweenDevices }}</SearchLabel></template>
|
<template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts.syncBetweenDevices }}</SearchLabel></template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<SearchMarker :keywords="['default', 'label', 'preset']">
|
|
||||||
<MkPreferenceContainer k="defaultWatermarkPresetId">
|
|
||||||
<MkSelect v-model="defaultWatermarkPresetId" :items="[{ label: i18n.ts.none, value: null }, ...prefer.r.watermarkPresets.value.map(p => ({ label: p.name || i18n.ts.noName, value: p.id }))]">
|
|
||||||
<template #label><SearchLabel>{{ i18n.ts.defaultPreset }}</SearchLabel></template>
|
|
||||||
</MkSelect>
|
|
||||||
</MkPreferenceContainer>
|
|
||||||
</SearchMarker>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
@ -219,7 +203,9 @@ import { computed, defineAsyncComponent, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import XWatermarkItem from './drive.WatermarkItem.vue';
|
import XWatermarkItem from './drive.WatermarkItem.vue';
|
||||||
|
import XImageFrameItem from './drive.ImageFrameItem.vue';
|
||||||
import type { WatermarkPreset } from '@/utility/watermark.js';
|
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 FormLink from '@/components/form/link.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
|
@ -239,6 +225,7 @@ import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
|
||||||
import { selectDriveFolder } from '@/utility/drive.js';
|
import { selectDriveFolder } from '@/utility/drive.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
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 => {
|
misskeyApi('drive').then(info => {
|
||||||
capacity.value = info.capacity;
|
capacity.value = info.capacity;
|
||||||
usage.value = info.usage;
|
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() {
|
async function addImageFramePreset() {
|
||||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), {
|
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), {
|
||||||
|
presetEditMode: true,
|
||||||
|
preset: null,
|
||||||
|
params: null,
|
||||||
}, {
|
}, {
|
||||||
ok: (preset: any) => {
|
presetOk: (preset) => {
|
||||||
//prefer.commit('imageFramePresets', [...prefer.s.imageFramePresets, preset]);
|
prefer.commit('imageFramePresets', [...prefer.s.imageFramePresets, preset]);
|
||||||
},
|
},
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,16 @@ export type ImageFramePreset = {
|
||||||
params: ImageFrameParams;
|
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 {
|
export class ImageFrameRenderer {
|
||||||
private compositor: ImageCompositor;
|
private compositor: ImageCompositor;
|
||||||
private image: HTMLImageElement | ImageBitmap;
|
private image: HTMLImageElement | ImageBitmap;
|
||||||
|
|
@ -47,11 +57,11 @@ export class ImageFrameRenderer {
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
image: HTMLImageElement | ImageBitmap,
|
image: HTMLImageElement | ImageBitmap,
|
||||||
exif: ExifReader.Tags,
|
exif: ExifReader.Tags | null,
|
||||||
renderAsPreview?: boolean,
|
renderAsPreview?: boolean,
|
||||||
}) {
|
}) {
|
||||||
this.image = options.image;
|
this.image = options.image;
|
||||||
this.exif = options.exif;
|
this.exif = options.exif ?? EXIF_MOCK;
|
||||||
this.renderAsPreview = options.renderAsPreview ?? false;
|
this.renderAsPreview = options.renderAsPreview ?? false;
|
||||||
console.log(this.exif);
|
console.log(this.exif);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue