summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkWidgetSettingsDialog.vue
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2026-03-05 10:56:50 +0000
committerGitHub <noreply@github.com>2026-03-05 10:56:50 +0000
commitfe3dd8edb5f30104cd0a7ed755eb254feda2922d (patch)
treeaf6cf5fa4ca75302ac2de5db742cead00bc13d21 /packages/frontend/src/components/MkWidgetSettingsDialog.vue
parentMerge pull request #16998 from misskey-dev/develop (diff)
parentRelease: 2026.3.0 (diff)
downloadmisskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.gz
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.bz2
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.zip
Merge pull request #17217 from misskey-dev/develop
Release: 2026.3.0
Diffstat (limited to 'packages/frontend/src/components/MkWidgetSettingsDialog.vue')
-rw-r--r--packages/frontend/src/components/MkWidgetSettingsDialog.vue174
1 files changed, 174 insertions, 0 deletions
diff --git a/packages/frontend/src/components/MkWidgetSettingsDialog.vue b/packages/frontend/src/components/MkWidgetSettingsDialog.vue
new file mode 100644
index 0000000000..292b4010ff
--- /dev/null
+++ b/packages/frontend/src/components/MkWidgetSettingsDialog.vue
@@ -0,0 +1,174 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+ ref="dialog"
+ :width="1000"
+ :height="600"
+ :scroll="false"
+ :withOkButton="true"
+ :okButtonDisabled="!canSave"
+ @close="cancel()"
+ @ok="save()"
+ @closed="emit('closed')"
+>
+ <template #header><i class="ti ti-icons"></i> {{ i18n.ts._widgets[widgetName] ?? widgetName }}</template>
+
+ <MkPreviewWithControls>
+ <template #preview>
+ <div :class="$style.previewWrapper">
+ <div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
+
+ <div ref="resizerRootEl" :class="$style.previewResizerRoot" inert>
+ <div
+ ref="resizerEl"
+ :class="$style.previewResizer"
+ :style="{ transform: widgetStyle }"
+ >
+ <component
+ :is="`widget-${widgetName}`"
+ :widget="{ name: widgetName, id: '__PREVIEW__', data: settings }"
+ ></component>
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <template #controls>
+ <div class="_spacer">
+ <MkForm v-model="settings" :form="form" @canSaveStateChange="onCanSaveStateChanged"/>
+ </div>
+ </template>
+ </MkPreviewWithControls>
+</MkModalWindow>
+</template>
+
+<script setup lang="ts">
+import { useTemplateRef, ref, computed, onBeforeUnmount, onMounted } from 'vue';
+import MkPreviewWithControls from './MkPreviewWithControls.vue';
+import type { Form } from '@/utility/form.js';
+import type { WidgetName } from '@/widgets/index.js';
+import { deepClone } from '@/utility/clone.js';
+import { i18n } from '@/i18n.js';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkForm from '@/components/MkForm.vue';
+
+const props = defineProps<{
+ widgetName: WidgetName;
+ form: Form;
+ currentSettings: Record<string, any>;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'saved', settings: Record<string, any>): void;
+ (ev: 'canceled'): void;
+ (ev: 'closed'): void;
+}>();
+
+const dialog = useTemplateRef('dialog');
+
+const settings = ref<Record<string, any>>(deepClone(props.currentSettings));
+
+const canSave = ref(true);
+
+function onCanSaveStateChanged(newCanSave: boolean) {
+ canSave.value = newCanSave;
+}
+
+function save() {
+ if (!canSave.value) return;
+ emit('saved', deepClone(settings.value));
+ dialog.value?.close();
+}
+
+function cancel() {
+ emit('canceled');
+ dialog.value?.close();
+}
+
+//#region プレビューのリサイズ
+const resizerRootEl = useTemplateRef('resizerRootEl');
+const resizerEl = useTemplateRef('resizerEl');
+const widgetHeight = ref(0);
+const widgetScale = ref(1);
+const widgetStyle = computed(() => {
+ return `translate(-50%, -50%) scale(${widgetScale.value})`;
+});
+const ro1 = new ResizeObserver(() => {
+ widgetHeight.value = resizerEl.value!.clientHeight;
+ calcScale();
+});
+const ro2 = new ResizeObserver(() => {
+ calcScale();
+});
+
+function calcScale() {
+ if (!resizerRootEl.value) return;
+ const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
+ const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ
+ const widgetWidth = 280;
+ const scale = Math.min(previewWidth / widgetWidth, previewHeight / widgetHeight.value, 1); // 拡大はしないので1を上限に
+ widgetScale.value = scale;
+}
+
+onMounted(() => {
+ if (resizerEl.value) {
+ ro1.observe(resizerEl.value);
+ }
+ if (resizerRootEl.value) {
+ ro2.observe(resizerRootEl.value);
+ }
+ calcScale();
+});
+
+onBeforeUnmount(() => {
+ ro1.disconnect();
+ ro2.disconnect();
+});
+//#endregion
+</script>
+
+<style module>
+.previewContainer {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+.previewTitle {
+ position: absolute;
+ z-index: 100;
+ top: 8px;
+ left: 8px;
+ padding: 6px 10px;
+ border-radius: 6px;
+ font-size: 85%;
+}
+
+.previewWrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ pointer-events: none;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+.previewResizerRoot {
+ position: relative;
+ flex: 1 0;
+}
+
+.previewResizer {
+ position: absolute;
+ container-type: inline-size;
+ top: 50%;
+ left: 50%;
+ width: 280px;
+}
+</style>