diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2026-03-05 10:56:50 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-05 10:56:50 +0000 |
| commit | fe3dd8edb5f30104cd0a7ed755eb254feda2922d (patch) | |
| tree | af6cf5fa4ca75302ac2de5db742cead00bc13d21 /packages/frontend/src/widgets | |
| parent | Merge pull request #16998 from misskey-dev/develop (diff) | |
| parent | Release: 2026.3.0 (diff) | |
| download | misskey-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/widgets')
30 files changed, 416 insertions, 154 deletions
diff --git a/packages/frontend/src/widgets/WidgetActivity.chart.vue b/packages/frontend/src/widgets/WidgetActivity.chart.vue index e708343b3a..bab688f851 100644 --- a/packages/frontend/src/widgets/WidgetActivity.chart.vue +++ b/packages/frontend/src/widgets/WidgetActivity.chart.vue @@ -53,19 +53,27 @@ const pointsReply = ref<string>(); const pointsRenote = ref<string>(); const pointsTotal = ref<string>(); -function dragListen(fn) { +function dragListen(fn: (ev: MouseEvent | TouchEvent) => void) { window.addEventListener('mousemove', fn); window.addEventListener('mouseleave', dragClear.bind(null, fn)); window.addEventListener('mouseup', dragClear.bind(null, fn)); } -function dragClear(fn) { +function dragClear(fn: (ev: MouseEvent | TouchEvent) => void) { window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); + window.removeEventListener('mouseleave', dragClear as any); + window.removeEventListener('mouseup', dragClear as any); } -function onMousedown(ev) { +function getPositionX(event: MouseEvent | TouchEvent) { + return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientX : 'clientX' in event ? event.clientX : 0; +} + +function getPositionY(event: MouseEvent | TouchEvent) { + return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientY : 'clientY' in event ? event.clientY : 0; +} + +function onMousedown(ev: MouseEvent) { const clickX = ev.clientX; const clickY = ev.clientY; const baseZoom = zoom.value; @@ -73,8 +81,11 @@ function onMousedown(ev) { // 動かした時 dragListen(me => { - let moveLeft = me.clientX - clickX; - let moveTop = me.clientY - clickY; + const x = getPositionX(me); + const y = getPositionY(me); + + let moveLeft = x - clickX; + let moveTop = y - clickY; zoom.value = Math.max(1, baseZoom + (-moveTop / 20)); pos.value = Math.min(0, basePos + moveLeft); diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index 9625abb4d1..3d0f4657b1 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -38,10 +38,12 @@ const name = 'activity'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, view: { diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue index 3951de1d84..c2a41b6257 100644 --- a/packages/frontend/src/widgets/WidgetAichan.vue +++ b/packages/frontend/src/widgets/WidgetAichan.vue @@ -12,14 +12,16 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import { useWidgetPropsManager } from './widget.js'; +import { i18n } from '@/i18n.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; -const name = 'ai'; +const name = 'aichan'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 795c5a2cfa..c6cafb270f 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -37,10 +37,12 @@ const name = 'aiscript'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, script: { type: 'string', + label: i18n.ts.script, multiline: true, default: '(1 + 1)', hidden: true, diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 18acd966fd..9ed441b77c 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -24,6 +24,7 @@ import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js'; import * as os from '@/os.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { $i } from '@/i.js'; +import { i18n } from '@/i18n.js'; import MkAsUi from '@/components/MkAsUi.vue'; import MkContainer from '@/components/MkContainer.vue'; import { registerAsUiLib } from '@/aiscript/ui.js'; @@ -33,11 +34,14 @@ const name = 'aiscriptApp'; const widgetPropsDef = { script: { type: 'string', + label: i18n.ts.script, multiline: true, + manualSave: true, default: '', }, showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue new file mode 100644 index 0000000000..2b714c2f6c --- /dev/null +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue @@ -0,0 +1,86 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <MkA :to="userPage(item.user)" style="overflow: clip;"> + <MkUserCardMini :user="item.user" :withChart="false" style="text-overflow: ellipsis; background: inherit; border-radius: unset;"> + <template #sub> + <span>{{ countdownDate }}</span> + <span> / </span> + <span class="_monospace">@{{ acct(item.user) }}</span> + </template> + </MkUserCardMini> + </MkA> + <button v-tooltip.noDelay="i18n.ts.note" class="_button" :class="$style.post" @click="os.post({initialText: `@${item.user.username}${item.user.host ? `@${item.user.host}` : ''} `})"> + <i class="ti-fw ti ti-confetti" :class="$style.postIcon"></i> + </button> +</div> +</template> + +<script setup lang="ts"> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import { useLowresTime } from '@/composables/use-lowres-time.js'; +import { userPage, acct } from '@/filters/user.js'; + +const props = defineProps<{ + item: Misskey.entities.UsersGetFollowingUsersByBirthdayResponse[number]; +}>(); + +const now = useLowresTime(); +const nowDate = computed(() => { + const date = new Date(now.value); + date.setHours(0, 0, 0, 0); + return date; +}); +const birthdayDate = computed(() => { + const [year, month, day] = props.item.birthday.split('-').map((v) => parseInt(v, 10)); + return new Date(year, month - 1, day, 0, 0, 0, 0); +}); + +const countdownDate = computed(() => { + const days = Math.floor((birthdayDate.value.getTime() - nowDate.value.getTime()) / (1000 * 60 * 60 * 24)); + if (days === 0) { + return i18n.ts.today; + } else if (days > 0) { + return i18n.tsx._timeIn.days({ n: days }); + } else { + return i18n.tsx._ago.daysAgo({ n: Math.abs(days) }); + } +}); +</script> + +<style lang="scss" module> +.root { + box-sizing: border-box; + display: grid; + align-items: center; + grid-template-columns: auto 56px; +} + +.post { + display: flex; + justify-content: center; + align-items: center; + height: 40px; + width: 40px; + margin-right: 16px; + aspect-ratio: 1/1; + border-radius: 100%; + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); + + &:hover { + background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); + } +} + +.postIcon { + color: var(--MI_THEME-fgOnAccent); +} +</style> diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index d1991cd70a..cf9c5a3d35 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -4,42 +4,75 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings"> +<MkContainer :style="`height: ${widgetProps.height}px;`" :showHeader="widgetProps.showHeader" :scrollable="true" class="mkw-bdayfollowings"> <template #icon><i class="ti ti-cake"></i></template> <template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ti ti-refresh"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="fetch"><i class="ti ti-refresh"></i></button></template> - <div :class="$style.bdayFRoot"> - <MkLoading v-if="fetching"/> - <div v-else-if="users.length > 0" :class="$style.bdayFGrid"> - <MkAvatar v-for="user in users" :key="user.id" :user="user.followee!" link preview></MkAvatar> + <MkPagination v-slot="{ items }" :paginator="birthdayUsersPaginator"> + <div> + <template v-for="(user, i) in items" :key="user.id"> + <div + v-if="i > 0 && isSeparatorNeeded(birthdayUsersPaginator.items.value[i - 1].birthday, user.birthday)" + > + <div :class="$style.date"> + <span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(birthdayUsersPaginator.items.value[i - 1].birthday, user.birthday)?.prevText }}</span> + <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> + <span>{{ getSeparatorInfo(birthdayUsersPaginator.items.value[i - 1].birthday, user.birthday)?.nextText }} <i class="ti ti-chevron-down"></i></span> + </div> + <XUser :class="$style.user" :item="user" /> + </div> + <XUser v-else :class="$style.user" :item="user" /> + </template> </div> - <div v-else :class="$style.bdayFFallback"> - <MkResult type="empty"/> - </div> - </div> + </MkPagination> </MkContainer> </template> <script lang="ts" setup> -import { ref } from 'vue'; -import * as Misskey from 'misskey-js'; -import { useInterval } from '@@/js/use-interval.js'; +import { computed, markRaw, ref, watch } from 'vue'; +import { useLowresTime } from '@/composables/use-lowres-time.js'; +import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import { misskeyApi } from '@/utility/misskey-api.js'; +import MkPagination from '@/components/MkPagination.vue'; +import XUser from './WidgetBirthdayFollowings.user.vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/i.js'; +import { Paginator } from '@/utility/paginator.js'; -const name = i18n.ts._widgets.birthdayFollowings; +const name = 'birthdayFollowings'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, + height: { + type: 'number' as const, + label: i18n.ts._widgetOptions.height, + default: 300, + }, + period: { + type: 'radio' as const, + label: i18n.ts._widgetOptions._birthdayFollowings.period, + default: '3day', + options: [{ + value: 'today' as const, + label: i18n.ts.today, + }, { + value: '3day' as const, + label: i18n.tsx.dayX({ day: 3 }), + }, { + value: 'week' as const, + label: i18n.ts.oneWeek, + }, { + value: 'month' as const, + label: i18n.ts.oneMonth, + }], + }, } satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -47,62 +80,84 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; const props = defineProps<WidgetComponentProps<WidgetProps>>(); const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const { widgetProps, configure } = useWidgetPropsManager(name, +const { widgetProps, configure } = useWidgetPropsManager( + name, widgetPropsDef, props, emit, ); -const users = ref<Misskey.Endpoints['users/following']['res']>([]); -const fetching = ref(true); -let lastFetchedAt = '1970-01-01'; +const now = useLowresTime(); +const nextDay = new Date(); +nextDay.setHours(24, 0, 0, 0); +let nextDayMidnightTime = nextDay.getTime(); -const fetch = () => { - if (!$i) { - users.value = []; - fetching.value = false; - return; +const begin = ref<Date>(new Date()); +const end = computed(() => { + switch (widgetProps.period) { + case '3day': + return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 3); + case 'week': + return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 7); + case 'month': + return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 30); + default: + return begin.value; } +}); + +const birthdayUsersPaginator = markRaw(new Paginator('users/get-following-users-by-birthday', { + limit: 18, + offsetMode: true, + computedParams: computed(() => { + if (widgetProps.period === 'today') { + return { + birthday: { + month: begin.value.getMonth() + 1, + day: begin.value.getDate(), + }, + }; + } else { + return { + birthday: { + begin: { + month: begin.value.getMonth() + 1, + day: begin.value.getDate(), + }, + end: { + month: end.value.getMonth() + 1, + day: end.value.getDate(), + }, + }, + }; + } + }), +})); - const lfAtD = new Date(lastFetchedAt); - lfAtD.setHours(0, 0, 0, 0); +function fetch() { const now = new Date(); - now.setHours(0, 0, 0, 0); + begin.value = now; +} - if (now > lfAtD) { - actualFetch(); +const UPDATE_INTERVAL = 1000 * 60; +let nextDayTimer: number | null = null; - lastFetchedAt = now.toISOString(); - } -}; +watch(now, (to) => { + // 次回更新までに日付が変わる場合、日付が変わった直後に強制的に更新するタイマーをセットする + if (nextDayMidnightTime - to <= UPDATE_INTERVAL) { + if (nextDayTimer != null) { + window.clearTimeout(nextDayTimer); + nextDayTimer = null; + } -function actualFetch() { - if ($i == null) { - users.value = []; - fetching.value = false; - return; + nextDayTimer = window.setTimeout(() => { + fetch(); + nextDay.setHours(24, 0, 0, 0); + nextDayMidnightTime = nextDay.getTime(); + nextDayTimer = null; + }, nextDayMidnightTime - to); } - - const now = new Date(); - now.setHours(0, 0, 0, 0); - fetching.value = true; - misskeyApi('users/following', { - limit: 18, - birthday: `${now.getFullYear().toString().padStart(4, '0')}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`, - userId: $i.id, - }).then(res => { - users.value = res; - window.setTimeout(() => { - // 早すぎるとチカチカする - fetching.value = false; - }, 100); - }); -} - -useInterval(fetch, 1000 * 60, { - immediate: true, - afterMounted: true, -}); +}, { immediate: true }); defineExpose<WidgetComponentExpose>({ name, @@ -112,24 +167,24 @@ defineExpose<WidgetComponentExpose>({ </script> <style lang="scss" module> -.bdayFRoot { - overflow: hidden; - min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--MI-margin) * 2)); +.root { + container-type: inline-size; + background: var(--MI_THEME-panel); } -.bdayFGrid { - display: grid; - grid-template-columns: repeat(6, 42px); - grid-template-rows: repeat(3, 42px); - place-content: center; - gap: 8px; - margin: var(--MI-margin) auto; + +.user { + border-bottom: solid 0.5px var(--MI_THEME-divider); } -.bdayFFallback { - height: 100%; +.date { display: flex; - flex-direction: column; - justify-content: center; + font-size: 85%; align-items: center; + justify-content: center; + gap: 1em; + opacity: 0.75; + padding: 8px 8px; + margin: 0 auto; + border-bottom: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index f8ae03c5fd..223901390e 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -20,22 +20,26 @@ import * as os from '@/os.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { $i } from '@/i.js'; import MkButton from '@/components/MkButton.vue'; +import { i18n } from '@/i18n.js'; const name = 'button'; const widgetPropsDef = { label: { type: 'string', + label: i18n.ts.label, default: 'BUTTON', }, colored: { type: 'boolean', + label: i18n.ts._widgetOptions._button.colored, default: true, }, script: { type: 'string', + label: i18n.ts.script, multiline: true, - default: 'Mk:dialog("hello" "world")', + default: 'Mk:dialog("hello", "world")', }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index f2321ca9fa..28336cbe09 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -50,6 +50,7 @@ const name = 'calendar'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetChat.vue b/packages/frontend/src/widgets/WidgetChat.vue index 8fee7f00f6..06d8f741f4 100644 --- a/packages/frontend/src/widgets/WidgetChat.vue +++ b/packages/frontend/src/widgets/WidgetChat.vue @@ -29,6 +29,7 @@ const name = 'chat'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue index 282a1a6d93..614e7c7fe5 100644 --- a/packages/frontend/src/widgets/WidgetClicker.vue +++ b/packages/frontend/src/widgets/WidgetClicker.vue @@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; +import { i18n } from '@/i18n.js'; import MkContainer from '@/components/MkContainer.vue'; import MkClickerGame from '@/components/MkClickerGame.vue'; @@ -23,6 +24,7 @@ const name = 'clicker'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: false, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue index 7aa69a39b5..80f312e7c4 100644 --- a/packages/frontend/src/widgets/WidgetClock.vue +++ b/packages/frontend/src/widgets/WidgetClock.vue @@ -44,10 +44,12 @@ const name = 'clock'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, size: { type: 'radio', + label: i18n.ts._widgetOptions._clock.size, default: 'medium', options: [{ value: 'small' as const, @@ -62,79 +64,89 @@ const widgetPropsDef = { }, thickness: { type: 'radio', + label: i18n.ts._widgetOptions._clock.thickness, default: 0.2, options: [{ value: 0.1 as const, - label: 'thin', + label: i18n.ts._widgetOptions._clock.thicknessThin, }, { value: 0.2 as const, - label: 'medium', + label: i18n.ts._widgetOptions._clock.thicknessMedium, }, { value: 0.3 as const, - label: 'thick', + label: i18n.ts._widgetOptions._clock.thicknessThick, }], }, graduations: { type: 'radio', + label: i18n.ts._widgetOptions._clock.graduations, default: 'numbers', options: [{ value: 'none' as const, - label: 'None', + label: i18n.ts.none, }, { value: 'dots' as const, - label: 'Dots', + label: i18n.ts._widgetOptions._clock.graduationDots, }, { value: 'numbers' as const, - label: 'Numbers', - }], + label: i18n.ts._widgetOptions._clock.graduationArabic, + }, /*, { + value: 'roman' as const, + label: i18n.ts._widgetOptions._clock.graduationRoman, + }*/], }, fadeGraduations: { type: 'boolean', + label: i18n.ts._widgetOptions._clock.fadeGraduations, default: true, }, sAnimation: { type: 'radio', + label: i18n.ts._widgetOptions._clock.sAnimation, default: 'elastic', options: [{ value: 'none' as const, - label: 'None', + label: i18n.ts.none, }, { value: 'elastic' as const, - label: 'Elastic', + label: i18n.ts._widgetOptions._clock.sAnimationElastic, }, { value: 'easeOut' as const, - label: 'Ease out', + label: i18n.ts._widgetOptions._clock.sAnimationEaseOut, }], }, twentyFour: { type: 'boolean', + label: i18n.ts._widgetOptions._clock.twentyFour, default: false, }, label: { type: 'radio', + label: i18n.ts.label, default: 'none', options: [{ value: 'none' as const, - label: 'None', + label: i18n.ts.none, }, { value: 'time' as const, - label: 'Time', + label: i18n.ts._widgetOptions._clock.labelTime, }, { value: 'tz' as const, - label: 'TZ', + label: i18n.ts._widgetOptions._clock.labelTz, }, { value: 'timeAndTz' as const, - label: 'Time + TZ', + label: i18n.ts._widgetOptions._clock.labelTimeAndTz, }], }, timezone: { type: 'enum', + label: i18n.ts._widgetOptions._clock.timezone, default: null, enum: [...timezones.map((tz) => ({ label: tz.name, value: tz.name.toLowerCase(), })), { - label: '(auto)', + label: i18n.ts.auto, value: null, }], }, diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue index b8cbc6429c..d50d4aef62 100644 --- a/packages/frontend/src/widgets/WidgetDigitalClock.vue +++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue @@ -19,6 +19,7 @@ import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { timezones } from '@/utility/timezones.js'; +import { i18n } from '@/i18n.js'; import MkDigitalClock from '@/components/MkDigitalClock.vue'; const name = 'digitalClock'; @@ -26,29 +27,34 @@ const name = 'digitalClock'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, fontSize: { type: 'number', + label: i18n.ts.fontSize, default: 1.5, step: 0.1, }, showMs: { type: 'boolean', + label: i18n.ts._widgetOptions._clock.showMs, default: true, }, showLabel: { type: 'boolean', + label: i18n.ts._widgetOptions._clock.showLabel, default: true, }, timezone: { type: 'enum', + label: i18n.ts._widgetOptions._clock.timezone, default: null, enum: [...timezones.map((tz) => ({ label: tz.name, value: tz.name.toLowerCase(), })), { - label: '(auto)', + label: i18n.ts.auto, value: null, }], }, diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index 3e880af03b..cf2c2f0ab7 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -43,6 +43,7 @@ const name = 'federation'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, } satisfies FormWithDefault; @@ -62,7 +63,7 @@ const instances = ref<Misskey.entities.FederationInstance[]>([]); const charts = ref<Misskey.entities.ChartsInstanceResponse[]>([]); const fetching = ref(true); -const fetch = async () => { +async function fetchInstances() { const fetchedInstances = await misskeyApi('federation/instances', { sort: '+latestRequestReceivedAt', limit: 5, @@ -71,14 +72,14 @@ const fetch = async () => { instances.value = fetchedInstances; charts.value = fetchedCharts; fetching.value = false; -}; +} -useInterval(fetch, 1000 * 60, { +useInterval(fetchInstances, 1000 * 60, { immediate: true, afterMounted: true, }); -function getInstanceIcon(instance): string { +function getInstanceIcon(instance: Misskey.entities.FederationInstance): string { return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png'; } diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index 8053dd43cf..c1e864bdb3 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -29,12 +29,14 @@ import MkTagCloud from '@/components/MkTagCloud.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; +import { i18n } from '@/i18n.js'; const name = 'instanceCloud'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, } satisfies FormWithDefault; @@ -53,7 +55,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, const cloud = useTemplateRef('cloud'); const activeInstances = shallowRef<Misskey.entities.FederationInstance[] | null>(null); -function onInstanceClick(i) { +function onInstanceClick(i: Misskey.entities.FederationInstance) { os.pageWindow(`/instance-info/${i.host}`); } @@ -70,7 +72,7 @@ useInterval(() => { afterMounted: true, }); -function getInstanceIcon(instance): string { +function getInstanceIcon(instance: Misskey.entities.FederationInstance): string { return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png'; } diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index fba7d82062..1727ea9b74 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, reactive, ref } from 'vue'; +import * as Misskey from 'misskey-js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; @@ -61,16 +62,19 @@ import * as sound from '@/utility/sound.js'; import { deepClone } from '@/utility/clone.js'; import { prefer } from '@/preferences.js'; import { genId } from '@/utility/id.js'; +import { i18n } from '@/i18n.js'; const name = 'jobQueue'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, sound: { type: 'boolean', + label: i18n.ts._widgetOptions._jobQueue.sound, default: false, }, } satisfies FormWithDefault; @@ -113,20 +117,22 @@ if (prefer.s['sound.masterVolume']) { } for (const domain of ['inbox', 'deliver']) { - prev[domain] = deepClone(current[domain]); + const d = domain as 'inbox' | 'deliver'; + prev[d] = deepClone(current[d]); } -const onStats = (stats) => { +const onStats = (stats: Misskey.entities.QueueStats) => { for (const domain of ['inbox', 'deliver']) { - prev[domain] = deepClone(current[domain]); - current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick; - current[domain].active = stats[domain].active; - current[domain].waiting = stats[domain].waiting; - current[domain].delayed = stats[domain].delayed; + const d = domain as 'inbox' | 'deliver'; + prev[d] = deepClone(current[d]); + current[d].activeSincePrevTick = stats[d].activeSincePrevTick; + current[d].active = stats[d].active; + current[d].waiting = stats[d].waiting; + current[d].delayed = stats[d].delayed; - if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) { + if (current[d].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) { const soundNode = sound.createSourceNode(jammedAudioBuffer.value, {}).soundSource; - if (soundNode) { + if (soundNode != null) { jammedSoundNodePlaying.value = true; soundNode.onended = () => jammedSoundNodePlaying.value = false; soundNode.start(); @@ -135,7 +141,7 @@ const onStats = (stats) => { } }; -const onStatsLog = (statsLog) => { +const onStatsLog = (statsLog: Misskey.entities.QueueStatsLog) => { for (const stats of [...statsLog].reverse()) { onStats(stats); } diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index 2beca8c43a..fd5b56991e 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -29,10 +29,12 @@ const name = 'memo'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, height: { type: 'number', + label: i18n.ts.height, default: 100, }, } satisfies FormWithDefault; @@ -50,7 +52,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, const text = ref<string | null>(store.s.memo); const changed = ref(false); -let timeoutId; +let timeoutId: number | null = null; const saveMemo = () => { store.set('memo', text.value); @@ -59,7 +61,7 @@ const saveMemo = () => { const onChange = () => { changed.value = true; - window.clearTimeout(timeoutId); + if (timeoutId != null) window.clearTimeout(timeoutId); timeoutId = window.setTimeout(saveMemo, 1000); }; diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue index b588bcb029..48a29d6145 100644 --- a/packages/frontend/src/widgets/WidgetNotifications.vue +++ b/packages/frontend/src/widgets/WidgetNotifications.vue @@ -31,10 +31,12 @@ const name = 'notifications'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, height: { type: 'number', + label: i18n.ts.height, default: 300, }, excludeTypes: { diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 9fd8c013d1..b0bb4b47b1 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -28,6 +28,7 @@ const name = 'onlineUsers'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: true, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index e89a642b99..670e764c8c 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -39,10 +39,12 @@ const name = 'photos'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, } satisfies FormWithDefault; @@ -62,12 +64,12 @@ const connection = useStream().useChannel('main'); const images = ref<Misskey.entities.DriveFile[]>([]); const fetching = ref(true); -const onDriveFileCreated = (file) => { +function onDriveFileCreated(file: Misskey.entities.DriveFile) { if (/^image\/.+$/.test(file.type)) { images.value.unshift(file); if (images.value.length > 9) images.value.pop(); } -}; +} const thumbnail = (image: Misskey.entities.DriveFile): string => { return prefer.s.disableShowingAnimatedImages diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index e5499aa0da..2495c5a6e9 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -25,28 +25,33 @@ import * as Misskey from 'misskey-js'; import { url as base } from '@@/js/config.js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; +import { i18n } from '@/i18n.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import { i18n } from '@/i18n.js'; const name = 'rss'; const widgetPropsDef = { url: { type: 'string', + label: i18n.ts._widgetOptions._rss.url, default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', + manualSave: true, }, refreshIntervalSec: { type: 'number', + label: i18n.ts._widgetOptions._rss.refreshIntervalSec, default: 60, }, maxEntries: { type: 'number', + label: i18n.ts._widgetOptions._rss.maxEntries, default: 15, }, showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, } satisfies FormWithDefault; @@ -68,7 +73,7 @@ const fetching = ref(true); const fetchEndpoint = computed(() => { const url = new URL('/api/fetch-rss', base); url.searchParams.set('url', widgetProps.url); - return url; + return url.toString(); }); const intervalClear = ref<(() => void) | undefined>(); @@ -83,7 +88,7 @@ const tick = () => { }); }; -watch(() => fetchEndpoint, tick); +watch(fetchEndpoint, tick); watch(() => widgetProps.refreshIntervalSec, () => { if (intervalClear.value) { intervalClear.value(); diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 95f82f7d7b..9ed21e6d00 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -35,6 +35,7 @@ import MkMarqueeText from '@/components/MkMarqueeText.vue'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { shuffle } from '@/utility/shuffle.js'; +import { i18n } from '@/i18n.js'; import { url as base } from '@@/js/config.js'; import { useInterval } from '@@/js/use-interval.js'; @@ -43,22 +44,28 @@ const name = 'rssTicker'; const widgetPropsDef = { url: { type: 'string', + label: i18n.ts._widgetOptions._rss.url, default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', + manualSave: true, }, shuffle: { type: 'boolean', + label: i18n.ts._widgetOptions._rssTicker.shuffle, default: true, }, refreshIntervalSec: { type: 'number', + label: i18n.ts._widgetOptions._rss.refreshIntervalSec, default: 60, }, maxEntries: { type: 'number', + label: i18n.ts._widgetOptions._rss.maxEntries, default: 15, }, duration: { type: 'range', + label: i18n.ts._widgetOptions._rssTicker.duration, default: 70, step: 1, min: 5, @@ -66,14 +73,17 @@ const widgetPropsDef = { }, reverse: { type: 'boolean', + label: i18n.ts._widgetOptions._rssTicker.reverse, default: false, }, showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: false, }, transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, } satisfies FormWithDefault; @@ -119,7 +129,7 @@ const tick = () => { }); }; -watch(() => fetchEndpoint, tick); +watch(fetchEndpoint, tick); watch(() => widgetProps.refreshIntervalSec, () => { if (intervalClear.value) { intervalClear.value(); diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index 240210c1fb..b812c89e08 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -33,6 +33,7 @@ const name = 'slideshow'; const widgetPropsDef = { height: { type: 'number', + label: i18n.ts._widgetOptions.height, default: 300, }, folderId: { @@ -95,11 +96,11 @@ const fetch = () => { }; const choose = () => { - selectDriveFolder(null).then(folder => { - if (folder[0] == null) { + selectDriveFolder(null).then(({ folders, canceled }) => { + if (canceled || folders[0] == null) { return; } - widgetProps.folderId = folder[0].id; + widgetProps.folderId = folders[0].id; save(); fetch(); }); diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index 6c775fd98c..83b8e7ccbc 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -41,13 +41,13 @@ import * as Misskey from 'misskey-js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; +import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; import { i18n } from '@/i18n.js'; import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; -import type { MenuItem } from '@/types/menu.js'; const name = 'timeline'; @@ -93,12 +93,12 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name, const menuOpened = ref(false); const headerTitle = computed<string>(() => { - if (widgetProps.src === 'list' && widgetProps.list != null) { - return widgetProps.list.name; - } else if (widgetProps.src === 'antenna' && widgetProps.antenna != null) { - return widgetProps.antenna.name; + if (widgetProps.src === 'list') { + return widgetProps.list != null ? widgetProps.list.name : '?'; + } else if (widgetProps.src === 'antenna') { + return widgetProps.antenna != null ? widgetProps.antenna.name : '?'; } else { - return i18n.ts._timelines[widgetProps.src]; + return i18n.ts._timelines[widgetProps.src] ?? '?'; } }); @@ -107,7 +107,7 @@ const setSrc = (src: TlSrc) => { save(); }; -const choose = async (ev: MouseEvent) => { +const choose = async (ev: PointerEvent) => { menuOpened.value = true; const [antennas, lists] = await Promise.all([ misskeyApi('antennas/list'), diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index dcb900b0c9..498129305b 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -36,11 +36,12 @@ import { misskeyApiGet } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; -const name = 'hashtags'; +const name = 'trends'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue index 226a4c73aa..1bb361664f 100644 --- a/packages/frontend/src/widgets/WidgetUnixClock.vue +++ b/packages/frontend/src/widgets/WidgetUnixClock.vue @@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, ref, watch } from 'vue'; import { useWidgetPropsManager } from './widget.js'; +import { i18n } from '@/i18n.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; @@ -26,19 +27,23 @@ const name = 'unixClock'; const widgetPropsDef = { transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, fontSize: { type: 'number', + label: i18n.ts.fontSize, default: 1.5, step: 0.1, }, showMs: { type: 'boolean', + label: i18n.ts._widgetOptions._clock.showMs, default: true, }, showLabel: { type: 'boolean', + label: i18n.ts._widgetOptions._clock.showLabel, default: true, }, } satisfies FormWithDefault; diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index 9e914fa648..3fc46f303f 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -41,6 +41,7 @@ const name = 'userList'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, listId: { diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index aea810d1ea..79bae68d71 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -42,7 +42,7 @@ export default function(app: App) { export const federationWidgets = [ 'federation', 'instanceCloud', -]; +] as const; export const widgets = [ 'profile', @@ -74,4 +74,6 @@ export const widgets = [ 'chat', ...federationWidgets, -]; +] as const; + +export type WidgetName = typeof widgets[number]; diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index f52b6fd12e..5d93c6a982 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -40,10 +40,12 @@ const name = 'serverMetric'; const widgetPropsDef = { showHeader: { type: 'boolean', + label: i18n.ts._widgetOptions.showHeader, default: true, }, transparent: { type: 'boolean', + label: i18n.ts._widgetOptions.transparent, default: false, }, view: { diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts index c5ca7ac26c..bfb724ff72 100644 --- a/packages/frontend/src/widgets/widget.ts +++ b/packages/frontend/src/widgets/widget.ts @@ -3,12 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { reactive, watch } from 'vue'; -import type { Reactive } from 'vue'; +import { defineAsyncComponent, reactive, watch } from 'vue'; import { throttle } from 'throttle-debounce'; +import type { Reactive } from 'vue'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; +import { getDefaultFormValues } from '@/utility/form.js'; import * as os from '@/os.js'; import { deepClone } from '@/utility/clone.js'; +import type { WidgetName } from './index.js'; export type Widget<P extends Record<string, unknown>> = { id: string; @@ -20,7 +22,7 @@ export type WidgetComponentProps<P extends Record<string, unknown>> = { }; export type WidgetComponentEmits<P extends Record<string, unknown>> = { - (ev: 'updateProps', props: P); + (ev: 'updateProps', props: P): void; }; export type WidgetComponentExpose = { @@ -30,7 +32,7 @@ export type WidgetComponentExpose = { }; export const useWidgetPropsManager = <F extends FormWithDefault>( - name: string, + name: WidgetName, propsDef: F, props: Readonly<WidgetComponentProps<GetFormResultType<F>>>, emit: WidgetComponentEmits<GetFormResultType<F>>, @@ -39,19 +41,23 @@ export const useWidgetPropsManager = <F extends FormWithDefault>( save: () => void; configure: () => void; } => { - const widgetProps = reactive<GetFormResultType<F>>((props.widget ? deepClone(props.widget.data) : {}) as GetFormResultType<F>); - - const mergeProps = () => { - for (const prop of Object.keys(propsDef)) { - if (typeof widgetProps[prop] === 'undefined') { - widgetProps[prop] = propsDef[prop].default; + const widgetProps = reactive((() => { + const np = getDefaultFormValues(propsDef); + if (props.widget?.data != null) { + for (const key of Object.keys(props.widget.data) as (keyof F)[]) { + np[key] = props.widget.data[key] as GetFormResultType<F>[typeof key]; } } - }; + return np; + })()); - watch(widgetProps, () => { - mergeProps(); - }, { deep: true, immediate: true }); + watch(() => props.widget?.data, (to) => { + if (to != null) { + for (const key of Object.keys(propsDef)) { + (widgetProps as any)[key] = to[key]; + } + } + }, { deep: true }); const save = throttle(3000, () => { emit('updateProps', widgetProps as GetFormResultType<F>); @@ -60,13 +66,38 @@ export const useWidgetPropsManager = <F extends FormWithDefault>( const configure = async () => { const form = deepClone(propsDef); for (const item of Object.keys(form)) { - form[item].default = widgetProps[item]; + form[item].default = (widgetProps as any)[item]; + } + + const res = await new Promise<{ + canceled: false; + result: GetFormResultType<F>; + } | { + canceled: true; + }>((resolve) => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWidgetSettingsDialog.vue')), { + widgetName: name, + form: form, + currentSettings: widgetProps, + }, { + saved: (newProps) => { + resolve({ canceled: false, result: newProps as GetFormResultType<F> }); + }, + canceled: () => { + resolve({ canceled: true }); + }, + closed: () => { + dispose(); + }, + }); + }); + + if (res.canceled) { + return; } - const { canceled, result } = await os.form(name, form); - if (canceled) return; - for (const key of Object.keys(result)) { - widgetProps[key] = result[key]; + for (const key of Object.keys(res.result)) { + (widgetProps as any)[key] = res.result[key]; } save(); |