This commit is contained in:
syuilo 2025-05-28 09:00:22 +09:00
parent 2a8920f8c3
commit 44212a31c9
5 changed files with 177 additions and 7 deletions

2
locales/index.d.ts vendored
View File

@ -12026,7 +12026,7 @@ export interface Locale extends ILocale {
*/ */
"opacity": string; "opacity": string;
/** /**
* *
*/ */
"scale": string; "scale": string;
/** /**

View File

@ -3221,7 +3221,7 @@ _watermarkEditor:
title: "ウォーターマークの編集" title: "ウォーターマークの編集"
repeat: "敷き詰める" repeat: "敷き詰める"
opacity: "不透明度" opacity: "不透明度"
scale: "大きさ" scale: "サイズ"
text: "テキスト" text: "テキスト"
position: "位置" position: "位置"
type: "タイプ" type: "タイプ"

View File

@ -56,6 +56,9 @@ import { selectFile } from '@/utility/drive.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { ensureSignin } from '@/i.js';
const $i = ensureSignin();
const props = defineProps<{ const props = defineProps<{
preset: WatermarkPreset | null; preset: WatermarkPreset | null;
@ -67,17 +70,17 @@ const preset = reactive(deepClone(props.preset) ?? {
layers: [{ layers: [{
id: uuid(), id: uuid(),
type: 'text', type: 'text',
text: 'sample', text: `(c) @${$i.username}`,
alignX: 'right', alignX: 'right',
alignY: 'bottom', alignY: 'bottom',
scale: 0.5, scale: 0.3,
opacity: 0.5, opacity: 0.75,
repeat: false, repeat: false,
}], }],
} satisfies WatermarkPreset); } satisfies WatermarkPreset);
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'ok'): void; (ev: 'ok', preset: WatermarkPreset): void;
(ev: 'cancel'): void; (ev: 'cancel'): void;
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
@ -126,6 +129,24 @@ onUnmounted(() => {
renderer = null; renderer = null;
} }
}); });
async function save() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts.name,
default: preset.name,
});
if (canceled) return;
preset.name = name || '';
dialog.value?.close();
if (renderer != null) {
renderer.destroy();
renderer = null;
}
emit('ok', preset);
}
</script> </script>
<style module> <style module>

View File

@ -0,0 +1,113 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkFolder :defaultOpen="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 { WatermarkPreset } from '@/utility/watermarker.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';
import { Watermarker } from '@/utility/watermarker.js';
const props = defineProps<{
preset: WatermarkPreset;
}>();
const emit = defineEmits<{
(ev: 'updatePreset', preset: WatermarkPreset): void,
(ev: 'del'): void,
}>();
async function edit() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWatermarkEditorDialog.vue')), {
preset: deepClone(props.preset),
}, {
ok: (preset: WatermarkPreset) => {
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: Watermarker | null = null;
onMounted(() => {
sampleImage.onload = async () => {
watch(canvasEl, async () => {
if (canvasEl.value == null) return;
renderer = new Watermarker({
canvas: canvasEl.value,
width: 1500,
height: 1000,
preset: props.preset,
originalImage: sampleImage,
});
await renderer.bakeTextures();
renderer.render();
}, { immediate: true });
};
});
onUnmounted(() => {
if (renderer != null) {
renderer.destroy();
renderer = null;
}
});
watch(() => props.preset, async () => {
if (renderer != null) {
renderer.updatePreset(props.preset);
renderer.render();
}
}, { 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>

View File

@ -58,7 +58,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-copyright"></i></template> <template #icon><i class="ti ti-copyright"></i></template>
<template #label><SearchLabel>{{ i18n.ts.watermark }}</SearchLabel></template> <template #label><SearchLabel>{{ i18n.ts.watermark }}</SearchLabel></template>
<MkButton iconOnly @click="addWatermarkPreset"><i class="ti ti-plus"></i></MkButton> <div class="_gaps_s">
<XWatermarkItem
v-for="(preset, i) in prefer.r.watermarkPresets.value"
:key="preset.id"
:preset="preset"
@updatePreset="onUpdateWatermarkPreset(preset.id, $event)"
@del="onDeleteWatermarkPreset(preset.id)"
/>
<MkButton iconOnly rounded style="margin: 0 auto;" @click="addWatermarkPreset"><i class="ti ti-plus"></i></MkButton>
</div>
</MkFolder> </MkFolder>
</SearchMarker> </SearchMarker>
@ -93,6 +103,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, defineAsyncComponent, ref } from 'vue'; 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 type { WatermarkPreset } from '@/utility/watermarker.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 FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
@ -166,10 +178,34 @@ function chooseUploadFolder() {
function addWatermarkPreset() { function addWatermarkPreset() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWatermarkEditorDialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWatermarkEditorDialog.vue')), {
}, { }, {
ok: (preset: WatermarkPreset) => {
prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]);
},
closed: () => dispose(), closed: () => dispose(),
}); });
} }
function onUpdateWatermarkPreset(id: string, preset: WatermarkPreset) {
const index = prefer.s.watermarkPresets.findIndex(p => p.id === id);
if (index !== -1) {
prefer.commit('watermarkPresets', [
...prefer.s.watermarkPresets.slice(0, index),
preset,
...prefer.s.watermarkPresets.slice(index + 1),
]);
}
}
function onDeleteWatermarkPreset(id: string) {
const index = prefer.s.watermarkPresets.findIndex(p => p.id === id);
if (index !== -1) {
prefer.commit('watermarkPresets', [
...prefer.s.watermarkPresets.slice(0, index),
...prefer.s.watermarkPresets.slice(index + 1),
]);
}
}
function saveProfile() { function saveProfile() {
misskeyApi('i/update', { misskeyApi('i/update', {
alwaysMarkNsfw: !!alwaysMarkNsfw.value, alwaysMarkNsfw: !!alwaysMarkNsfw.value,