diff options
Diffstat (limited to 'packages/frontend/src/pages/settings')
3 files changed, 204 insertions, 10 deletions
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..62922fc964 --- /dev/null +++ b/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue @@ -0,0 +1,113 @@ +<!-- +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/ImageFrameRenderer.js'; +import { ImageFrameRenderer } from '@/utility/image-frame-renderer/ImageFrameRenderer.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, + caption: 'Example caption', + filename: 'example_file_name.jpg', + 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> diff --git a/packages/frontend/src/pages/settings/drive.WatermarkItem.vue b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue index bb91d5e212..0c03a4493a 100644 --- a/packages/frontend/src/pages/settings/drive.WatermarkItem.vue +++ b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue @@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'; -import type { WatermarkPreset } from '@/utility/watermark.js'; -import { WatermarkRenderer } from '@/utility/watermark.js'; +import type { WatermarkPreset } from '@/utility/watermark/WatermarkRenderer.js'; +import { WatermarkRenderer } from '@/utility/watermark/WatermarkRenderer.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -41,9 +41,11 @@ const emit = defineEmits<{ async function edit() { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWatermarkEditorDialog.vue')), { + presetEditMode: true, preset: deepClone(props.preset), + layers: deepClone(props.preset.layers), }, { - ok: (preset) => { + presetOk: (preset) => { emit('updatePreset', preset); }, closed: () => dispose(), @@ -78,9 +80,7 @@ onMounted(() => { image: sampleImage, }); - await renderer.setLayers(props.preset.layers); - - renderer.render(); + await renderer.render(props.preset.layers); }, { immediate: true }); }; }); @@ -94,8 +94,7 @@ onUnmounted(() => { watch(() => props.preset, async () => { if (renderer != null) { - await renderer.setLayers(props.preset.layers); - renderer.render(); + await renderer.render(props.preset.layers); } }, { deep: true }); </script> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index f58ff4c78c..8d443921a9 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -124,6 +124,34 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> </SearchMarker> + <SearchMarker :keywords="['label', 'frame', 'credit', 'metadata']"> + <MkFolder> + <template #icon><i class="ti ti-device-ipad-horizontal"></i></template> + <template #label><SearchLabel>{{ i18n.ts.frame }}</SearchLabel></template> + <template #caption>{{ i18n.ts._imageFrameEditor.tip }}</template> + + <div class="_gaps"> + <div class="_gaps_s"> + <XImageFrameItem + v-for="(preset, i) in prefer.r.imageFramePresets.value" + :key="preset.id" + :preset="preset" + @updatePreset="onUpdateImageFramePreset(preset.id, $event)" + @del="onDeleteImageFramePreset(preset.id)" + /> + + <MkButton iconOnly rounded style="margin: 0 auto;" @click="addImageFramePreset"><i class="ti ti-plus"></i></MkButton> + + <SearchMarker :keywords="['sync', 'frame', 'label', 'preset', 'devices']"> + <MkSwitch :modelValue="imageFramePresetsSyncEnabled" @update:modelValue="changeImageFramePresetsSyncEnabled"> + <template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts.syncBetweenDevices }}</SearchLabel></template> + </MkSwitch> + </SearchMarker> + </div> + </div> + </MkFolder> + </SearchMarker> + <SearchMarker :keywords="['default', 'image', 'compression']"> <MkPreferenceContainer k="defaultImageCompressionLevel"> <MkSelect @@ -175,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 type { WatermarkPreset } from '@/utility/watermark.js'; +import XImageFrameItem from './drive.ImageFrameItem.vue'; +import type { WatermarkPreset } from '@/utility/watermark/WatermarkRenderer.js'; +import type { ImageFramePreset } from '@/utility/image-frame-renderer/ImageFrameRenderer.js'; import FormLink from '@/components/form/link.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -195,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(); @@ -236,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; @@ -266,8 +311,11 @@ function chooseUploadFolder() { async function addWatermarkPreset() { const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkWatermarkEditorDialog.vue').then(x => x.default), { + presetEditMode: true, + preset: null, + layers: [], }, { - ok: (preset: WatermarkPreset) => { + presetOk: (preset) => { prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]); }, closed: () => dispose(), @@ -299,6 +347,40 @@ 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, + }, { + presetOk: (preset) => { + prefer.commit('imageFramePresets', [...prefer.s.imageFramePresets, preset]); + }, + closed: () => dispose(), + }); +} + function saveProfile() { misskeyApi('i/update', { alwaysMarkNsfw: !!alwaysMarkNsfw.value, |