summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages/settings
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/pages/settings')
-rw-r--r--packages/frontend/src/pages/settings/drive.ImageFrameItem.vue113
-rw-r--r--packages/frontend/src/pages/settings/drive.WatermarkItem.vue15
-rw-r--r--packages/frontend/src/pages/settings/drive.vue86
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,