diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-07-06 19:36:11 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-06 19:36:11 +0900 |
| commit | a8abb03d1785791ab40e57ab49c87640914532c9 (patch) | |
| tree | f80ea7a393a278e29f9642e86be8b341fcb4b95b /packages | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | misskey-a8abb03d1785791ab40e57ab49c87640914532c9.tar.gz misskey-a8abb03d1785791ab40e57ab49c87640914532c9.tar.bz2 misskey-a8abb03d1785791ab40e57ab49c87640914532c9.zip | |
refactor(frontend): Formまわりの型強化 (#16260)
* refactor(frontend): Formまわりの型強化
* fix
* avoid non-null assertion and add null check for safety
* refactor
* avoid non-null assertion and add null check for safety
* Update clip.vue
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages')
45 files changed, 344 insertions, 239 deletions
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 83ad0ebdf9..bf0e5e1b37 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -13,7 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; import type { MenuItem } from '@/types/menu.js'; import type { PostFormProps } from '@/types/post-form.js'; -import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue'; +import type { UploaderFeatures } from '@/composables/use-uploader.js'; import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -837,7 +837,7 @@ export function launchUploader( options?: { folderId?: string | null; multiple?: boolean; - features?: UploaderDialogFeatures; + features?: UploaderFeatures; }, ): Promise<Misskey.entities.DriveFile[]> { return new Promise(async (res, rej) => { diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 8843812544..8176fb519b 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -76,7 +76,8 @@ watch(() => props.clipId, async () => { clip.value = await misskeyApi('clips/show', { clipId: props.clipId, }); - favorited.value = clip.value.isFavorited; + + favorited.value = clip.value!.isFavorited ?? false; }, { immediate: true, }); @@ -108,6 +109,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ icon: 'ti ti-pencil', text: i18n.ts.edit, handler: async (): Promise<void> => { + if (clip.value == null) return; + const { canceled, result } = await os.form(clip.value.name, { name: { type: 'string', @@ -128,6 +131,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ default: clip.value.isPublic, }, }); + if (canceled) return; os.apiWithDialog('clips/update', { @@ -178,6 +182,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ text: i18n.ts.delete, danger: true, handler: async (): Promise<void> => { + if (clip.value == null) return; + const { canceled } = await os.confirm({ type: 'warning', text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }), diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 4c664a0951..f48dc5be4d 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -64,6 +64,7 @@ async function create() { default: false, }, }); + if (canceled) return; os.apiWithDialog('clips/create', result); diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index 39575fe1f7..8eb2ab9fd0 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -79,7 +79,9 @@ async function createKey() { default: scope.value.join('/'), }, }); + if (canceled) return; + os.apiWithDialog('i/registry/set', { scope: result.scope.split('/'), key: result.key, diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 5e59082b50..3762dadd12 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -56,7 +56,9 @@ async function createKey() { label: i18n.ts._registry.scope, }, }); + if (canceled) return; + os.apiWithDialog('i/registry/set', { scope: result.scope.split('/'), key: result.key, diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index 6010180e68..d6007a27ed 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -14,12 +14,13 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; +import type { FormWithDefault } from '@/utility/form.js'; export type Plugin = { installId: string; name: string; active: boolean; - config?: Record<string, { default: any }>; + config?: FormWithDefault; configData: Record<string, any>; src: string | null; version: string; @@ -240,7 +241,7 @@ async function launchPlugin(id: Plugin['installId']): Promise<void> { pluginLogs.value.set(plugin.installId, []); function systemLog(message: string, isError = false): void { - pluginLogs.value.get(plugin.installId)?.push({ + pluginLogs.value.get(plugin!.installId)?.push({ at: Date.now(), isSystem: true, message, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 9afaf2c9b9..e9402cfb70 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -29,7 +29,7 @@ export const store = markRaw(new Pizzax('base', { }, memo: { where: 'account', - default: null, + default: null as string | null, }, reactionAcceptance: { where: 'account', diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts index 728c0d0d29..e9b14e70ee 100644 --- a/packages/frontend/src/ui/deck/tl-note-notification.ts +++ b/packages/frontend/src/ui/deck/tl-note-notification.ts @@ -29,7 +29,8 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis label: i18n.ts.sound, default: soundSetting.value.type ?? 'none', enum: soundsTypes.map(f => ({ - value: f ?? 'none', label: getSoundTypeName(f), + value: f ?? 'none' as Exclude<SoundType, null> | 'none', + label: getSoundTypeName(f), })), }, soundFile: { @@ -81,16 +82,17 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis }, }, }); + if (canceled) return; const res = buildSoundStore(result); if (res) soundSetting.value = res; - function buildSoundStore(result: any): SoundStore | null { - const type = (result.type === 'none' ? null : result.type) as SoundType; - const volume = result.volume as number; - const fileId = result.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined); - const fileUrl = result.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined); + function buildSoundStore(r: NonNullable<typeof result>): SoundStore | null { + const type = (r.type === 'none' ? null : r.type); + const volume = r.volume; + const fileId = r.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined); + const fileUrl = r.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined); if (type === '_driveFile_') { if (!fileUrl || !fileId) { diff --git a/packages/frontend/src/utility/form.ts b/packages/frontend/src/utility/form.ts index 1032e97ac9..2b765dc714 100644 --- a/packages/frontend/src/utility/form.ts +++ b/packages/frontend/src/utility/form.ts @@ -5,55 +5,59 @@ import * as Misskey from 'misskey-js'; -type EnumItem = string | { +export type EnumItem = string | { label: string; - value: string; + value: unknown; }; type Hidden = boolean | ((v: any) => boolean); -export type FormItem = { +interface FormItemBase { label?: string; + hidden?: Hidden; +} + +export interface StringFormItem extends FormItemBase { type: 'string'; default?: string | null; description?: string; required?: boolean; - hidden?: Hidden; multiline?: boolean; treatAsMfm?: boolean; -} | { - label?: string; +} + +export interface NumberFormItem extends FormItemBase { type: 'number'; default?: number | null; description?: string; required?: boolean; - hidden?: Hidden; step?: number; -} | { - label?: string; +} + +export interface BooleanFormItem extends FormItemBase { type: 'boolean'; default?: boolean | null; description?: string; - hidden?: Hidden; -} | { - label?: string; +} + +export interface EnumFormItem extends FormItemBase { type: 'enum'; default?: string | null; required?: boolean; - hidden?: Hidden; enum: EnumItem[]; -} | { - label?: string; +} + +export interface RadioFormItem extends FormItemBase { type: 'radio'; default?: unknown | null; required?: boolean; - hidden?: Hidden; options: { label: string; value: unknown; }[]; -} | { - label?: string; +} + +export interface RangeFormItem extends FormItemBase { type: 'range'; default?: number | null; description?: string; @@ -62,42 +66,80 @@ export type FormItem = { min: number; max: number; textConverter?: (value: number) => string; - hidden?: Hidden; -} | { - label?: string; +} + +export interface ObjectFormItem extends FormItemBase { type: 'object'; default?: Record<string, unknown> | null; - hidden: Hidden; -} | { - label?: string; +} + +export interface ArrayFormItem extends FormItemBase { type: 'array'; default?: unknown[] | null; - hidden: Hidden; -} | { +} + +export interface ButtonFormItem extends FormItemBase { type: 'button'; content?: string; - hidden?: Hidden; action: (ev: MouseEvent, v: any) => void; -} | { +} + +export interface DriveFileFormItem extends FormItemBase { type: 'drive-file'; defaultFileId?: string | null; - hidden?: Hidden; validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>; -}; +} + +export type FormItem = + StringFormItem | + NumberFormItem | + BooleanFormItem | + EnumFormItem | + RadioFormItem | + RangeFormItem | + ObjectFormItem | + ArrayFormItem | + ButtonFormItem | + DriveFileFormItem; export type Form = Record<string, FormItem>; +export type FormItemWithDefault = FormItem & { + default: unknown; +}; + +export type FormWithDefault = Record<string, FormItemWithDefault>; + +type GetRadioItemType<Item extends RadioFormItem = RadioFormItem> = Item['options'][number]['value']; +type GetEnumItemType<Item extends EnumFormItem, E = Item['enum'][number]> = E extends { value: unknown } ? E['value'] : E; + +type InferDefault<T, Fallback> = T extends { default: infer D } + ? D extends undefined ? Fallback : D + : Fallback; + +type NonNullableIfRequired<T, Item extends FormItem> = + Item extends { required: false } ? T | null | undefined : NonNullable<T>; + type GetItemType<Item extends FormItem> = - Item['type'] extends 'string' ? string : - Item['type'] extends 'number' ? number : - Item['type'] extends 'boolean' ? boolean : - Item['type'] extends 'radio' ? unknown : - Item['type'] extends 'range' ? number : - Item['type'] extends 'enum' ? string : - Item['type'] extends 'array' ? unknown[] : - Item['type'] extends 'object' ? Record<string, unknown> : - Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined : - never; + Item extends StringFormItem + ? NonNullableIfRequired<InferDefault<Item, string>, Item> + : Item extends NumberFormItem + ? NonNullableIfRequired<InferDefault<Item, number>, Item> + : Item extends BooleanFormItem + ? boolean + : Item extends RadioFormItem + ? GetRadioItemType<Item> + : Item extends RangeFormItem + ? NonNullableIfRequired<InferDefault<RangeFormItem, number>, Item> + : Item extends EnumFormItem + ? GetEnumItemType<Item> + : Item extends ArrayFormItem + ? NonNullableIfRequired<InferDefault<ArrayFormItem, unknown[]>, Item> + : Item extends ObjectFormItem + ? NonNullableIfRequired<InferDefault<Item, Record<string, unknown>>, Item> + : Item extends DriveFileFormItem + ? Misskey.entities.DriveFile | undefined + : never; export type GetFormResultType<F extends Form> = { [P in keyof F]: GetItemType<F[P]>; diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts index ea93444f08..5361c1252d 100644 --- a/packages/frontend/src/utility/get-note-menu.ts +++ b/packages/frontend/src/utility/get-note-menu.ts @@ -101,7 +101,7 @@ export async function getNoteClipMenu(props: { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - default: null, + default: null as string | null, label: i18n.ts.name, }, description: { diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts index 5c08b8c462..ad0864019b 100644 --- a/packages/frontend/src/utility/get-user-menu.ts +++ b/packages/frontend/src/utility/get-user-menu.ts @@ -132,6 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router const userDetailed = await misskeyApi('users/show', { userId: user.id, }); + const { canceled, result } = await os.form(i18n.ts.editMemo, { memo: { type: 'string', @@ -141,6 +142,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router default: userDetailed.memo, }, }); + if (canceled) return; os.apiWithDialog('users/update-memo', { diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index db03d1406c..9625abb4d1 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -25,29 +25,31 @@ import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; import XCalendar from './WidgetActivity.calendar.vue'; import XChart from './WidgetActivity.chart.vue'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { misskeyApiGet } from '@/utility/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; -import { $i } from '@/i.js'; +import { ensureSignin } from '@/i.js'; import { i18n } from '@/i18n.js'; +const $i = ensureSignin(); + const name = 'activity'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, view: { - type: 'number' as const, + type: 'number', default: 0, hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue index 2bc7facc88..3951de1d84 100644 --- a/packages/frontend/src/widgets/WidgetAichan.vue +++ b/packages/frontend/src/widgets/WidgetAichan.vue @@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; const name = 'ai'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -42,6 +42,8 @@ const touched = () => { }; const onMousemove = (ev: MouseEvent) => { + if (!live2d.value || !live2d.value.contentWindow) return; + const iframeRect = live2d.value.getBoundingClientRect(); live2d.value.contentWindow.postMessage({ type: 'moveCursor', diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index da71dcad29..a2d964718e 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -23,7 +23,7 @@ import { ref } from 'vue'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import MkContainer from '@/components/MkContainer.vue'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; @@ -35,16 +35,16 @@ const name = 'aiscript'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, script: { - type: 'string' as const, + type: 'string', multiline: true, default: '(1 + 1)', hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -106,7 +106,7 @@ const run = async () => { } catch (err) { os.alert({ type: 'error', - text: err, + text: err instanceof Error ? err.message : String(err), }); } }; diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 429b0e0ffb..fdd4eaae06 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -18,7 +18,7 @@ import type { Ref } from 'vue'; import { Interpreter, Parser } from '@syuilo/aiscript'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { $i } from '@/i.js'; @@ -31,15 +31,15 @@ const name = 'aiscriptApp'; const widgetPropsDef = { script: { - type: 'string' as const, + type: 'string', multiline: true, default: '', }, showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -92,7 +92,7 @@ async function run() { os.alert({ type: 'error', title: 'AiScript Error', - text: err.message, + text: err instanceof Error ? err.message : String(err), }); } } diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 4790f143cb..d1991cd70a 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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> + <MkAvatar v-for="user in users" :key="user.id" :user="user.followee!" link preview></MkAvatar> </div> <div v-else :class="$style.bdayFFallback"> <MkResult type="empty"/> @@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -37,10 +37,10 @@ const name = i18n.ts._widgets.birthdayFollowings; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index 4afe735a22..e88a960f87 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { Interpreter, Parser } from '@syuilo/aiscript'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { $i } from '@/i.js'; @@ -25,19 +25,19 @@ const name = 'button'; const widgetPropsDef = { label: { - type: 'string' as const, + type: 'string', default: 'BUTTON', }, colored: { - type: 'boolean' as const, + type: 'boolean', default: true, }, script: { - type: 'string' as const, + type: 'string', multiline: true, default: 'Mk:dialog("hello" "world")', }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -81,7 +81,7 @@ const run = async () => { } catch (err) { os.alert({ type: 'error', - text: err, + text: err instanceof Error ? err.message : String(err), }); } }; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 54f78469b2..12c0a66c5c 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { i18n } from '@/i18n.js'; import { useInterval } from '@@/js/use-interval.js'; @@ -49,10 +49,10 @@ const name = 'calendar'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetChat.vue b/packages/frontend/src/widgets/WidgetChat.vue index 43b2a6e522..8fee7f00f6 100644 --- a/packages/frontend/src/widgets/WidgetChat.vue +++ b/packages/frontend/src/widgets/WidgetChat.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { i18n } from '@/i18n.js'; import MkChatHistories from '@/components/MkChatHistories.vue'; @@ -28,10 +28,10 @@ const name = 'chat'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue index 87ffd3d732..282a1a6d93 100644 --- a/packages/frontend/src/widgets/WidgetClicker.vue +++ b/packages/frontend/src/widgets/WidgetClicker.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkClickerGame from '@/components/MkClickerGame.vue'; @@ -22,10 +22,10 @@ const name = 'clicker'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue index 826ecf6e02..7aa69a39b5 100644 --- a/packages/frontend/src/widgets/WidgetClock.vue +++ b/packages/frontend/src/widgets/WidgetClock.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkAnalogClock from '@/components/MkAnalogClock.vue'; import MkDigitalClock from '@/components/MkDigitalClock.vue'; @@ -43,76 +43,92 @@ const name = 'clock'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, size: { - type: 'radio' as const, + type: 'radio', default: 'medium', options: [{ - value: 'small', label: i18n.ts.small, + value: 'small' as const, + label: i18n.ts.small, }, { - value: 'medium', label: i18n.ts.medium, + value: 'medium' as const, + label: i18n.ts.medium, }, { - value: 'large', label: i18n.ts.large, + value: 'large' as const, + label: i18n.ts.large, }], }, thickness: { - type: 'radio' as const, + type: 'radio', default: 0.2, options: [{ - value: 0.1, label: 'thin', + value: 0.1 as const, + label: 'thin', }, { - value: 0.2, label: 'medium', + value: 0.2 as const, + label: 'medium', }, { - value: 0.3, label: 'thick', + value: 0.3 as const, + label: 'thick', }], }, graduations: { - type: 'radio' as const, + type: 'radio', default: 'numbers', options: [{ - value: 'none', label: 'None', + value: 'none' as const, + label: 'None', }, { - value: 'dots', label: 'Dots', + value: 'dots' as const, + label: 'Dots', }, { - value: 'numbers', label: 'Numbers', + value: 'numbers' as const, + label: 'Numbers', }], }, fadeGraduations: { - type: 'boolean' as const, + type: 'boolean', default: true, }, sAnimation: { - type: 'radio' as const, + type: 'radio', default: 'elastic', options: [{ - value: 'none', label: 'None', + value: 'none' as const, + label: 'None', }, { - value: 'elastic', label: 'Elastic', + value: 'elastic' as const, + label: 'Elastic', }, { - value: 'easeOut', label: 'Ease out', + value: 'easeOut' as const, + label: 'Ease out', }], }, twentyFour: { - type: 'boolean' as const, + type: 'boolean', default: false, }, label: { - type: 'radio' as const, + type: 'radio', default: 'none', options: [{ - value: 'none', label: 'None', + value: 'none' as const, + label: 'None', }, { - value: 'time', label: 'Time', + value: 'time' as const, + label: 'Time', }, { - value: 'tz', label: 'TZ', + value: 'tz' as const, + label: 'TZ', }, { - value: 'timeAndTz', label: 'Time + TZ', + value: 'timeAndTz' as const, + label: 'Time + TZ', }], }, timezone: { - type: 'enum' as const, + type: 'enum', default: null, enum: [...timezones.map((tz) => ({ label: tz.name, @@ -122,7 +138,7 @@ const widgetPropsDef = { value: null, }], }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue index d79ec79d4f..b8cbc6429c 100644 --- a/packages/frontend/src/widgets/WidgetDigitalClock.vue +++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { timezones } from '@/utility/timezones.js'; import MkDigitalClock from '@/components/MkDigitalClock.vue'; @@ -25,24 +25,24 @@ const name = 'digitalClock'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, fontSize: { - type: 'number' as const, + type: 'number', default: 1.5, step: 0.1, }, showMs: { - type: 'boolean' as const, + type: 'boolean', default: true, }, showLabel: { - type: 'boolean' as const, + type: 'boolean', default: true, }, timezone: { - type: 'enum' as const, + type: 'enum', default: null, enum: [...timezones.map((tz) => ({ label: tz.name, @@ -52,7 +52,7 @@ const widgetPropsDef = { value: null, }], }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index ca6f27bd09..3e880af03b 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkContainer :showHeader="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation"> +<MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-federation class="mkw-federation"> <template #icon><i class="ti ti-whirl"></i></template> <template #header>{{ i18n.ts._widgets.federation }}</template> @@ -30,7 +30,7 @@ import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; @@ -42,10 +42,10 @@ const name = 'federation'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index 0c9f98f9e3..8053dd43cf 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -23,7 +23,7 @@ import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkTagCloud from '@/components/MkTagCloud.vue'; import * as os from '@/os.js'; @@ -34,10 +34,10 @@ const name = 'instanceCloud'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index 8d721298d5..722e6fadb2 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_panel"> - <div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ instance.bannerUrl })` : null }"> + <div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ instance.bannerUrl })` : undefined }"> <div :class="$style.iconContainer"> <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/> </div> @@ -22,14 +22,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { host } from '@@/js/config.js'; import { instance } from '@/instance.js'; const name = 'instanceInfo'; const widgetPropsDef = { -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 84fd4669cd..fba7d82062 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onUnmounted, reactive, ref } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { useStream } from '@/stream.js'; import kmg from '@/filters/kmg.js'; import * as sound from '@/utility/sound.js'; @@ -66,14 +66,14 @@ const name = 'jobQueue'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, sound: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index 3df5c5bfd7..2beca8c43a 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header>{{ i18n.ts._widgets.memo }}</template> <div :class="$style.root"> - <textarea v-model="text" :style="`height: ${widgetProps.height}px;`" :class="$style.textarea" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea> + <textarea v-model="text" :style="`height: ${widgetProps.height}px;`" :class="$style.textarea" :placeholder="i18n.ts.memo" @input="onChange"></textarea> <button :class="$style.save" :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button> </div> </MkContainer> @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, watch } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -28,14 +28,14 @@ const name = 'memo'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, height: { - type: 'number' as const, + type: 'number', default: 100, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue index a1af5d084c..8fb7968238 100644 --- a/packages/frontend/src/widgets/WidgetNotifications.vue +++ b/packages/frontend/src/widgets/WidgetNotifications.vue @@ -17,9 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent } from 'vue'; +import type { notificationTypes as notificationTypes_typeReferenceOnly } from '@@/js/const.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue'; import * as os from '@/os.js'; @@ -29,19 +30,19 @@ const name = 'notifications'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, height: { - type: 'number' as const, + type: 'number', default: 300, }, excludeTypes: { - type: 'array' as const, + type: 'array', hidden: true, - default: [], + default: [] as (typeof notificationTypes_typeReferenceOnly[number])[], }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index ce1871420a..9fd8c013d1 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; -import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; +import { misskeyApiGet } from '@/utility/misskey-api.js'; import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import number from '@/filters/number.js'; @@ -27,10 +27,10 @@ const name = 'onlineUsers'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index 6bbc52b2c2..e89a642b99 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -26,7 +26,7 @@ import { onUnmounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { useStream } from '@/stream.js'; import { getStaticImageUrl } from '@/utility/media-proxy.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -38,14 +38,14 @@ const name = 'photos'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue index 3170eab305..9f464cac95 100644 --- a/packages/frontend/src/widgets/WidgetPostForm.vue +++ b/packages/frontend/src/widgets/WidgetPostForm.vue @@ -11,13 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only import { } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkPostForm from '@/components/MkPostForm.vue'; const name = 'postForm'; const widgetPropsDef = { -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue index 3fe8378a39..be149dae35 100644 --- a/packages/frontend/src/widgets/WidgetProfile.vue +++ b/packages/frontend/src/widgets/WidgetProfile.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_panel"> - <div :class="$style.container" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> + <div :class="$style.container" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : undefined }"> <div :class="$style.avatarContainer"> <MkAvatar :class="$style.avatar" :user="$i"/> </div> @@ -24,14 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; -import { $i } from '@/i.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; +import { ensureSignin } from '@/i.js'; import { userPage } from '@/filters/user.js'; +const $i = ensureSignin(); + const name = 'profile'; const widgetPropsDef = { -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 2594262df1..e5499aa0da 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -26,7 +26,7 @@ import { url as base } from '@@/js/config.js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { i18n } from '@/i18n.js'; @@ -34,22 +34,22 @@ const name = 'rss'; const widgetPropsDef = { url: { - type: 'string' as const, + type: 'string', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', }, refreshIntervalSec: { - type: 'number' as const, + type: 'number', default: 60, }, maxEntries: { - type: 'number' as const, + type: 'number', default: 15, }, showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 7fe7c6111a..9d4feb784c 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -32,7 +32,7 @@ import * as Misskey from 'misskey-js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import MarqueeText from '@/components/MkMarqueeText.vue'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { shuffle } from '@/utility/shuffle.js'; import { url as base } from '@@/js/config.js'; @@ -42,41 +42,41 @@ const name = 'rssTicker'; const widgetPropsDef = { url: { - type: 'string' as const, + type: 'string', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', }, shuffle: { - type: 'boolean' as const, + type: 'boolean', default: true, }, refreshIntervalSec: { - type: 'number' as const, + type: 'number', default: 60, }, maxEntries: { - type: 'number' as const, + type: 'number', default: 15, }, duration: { - type: 'range' as const, + type: 'range', default: 70, step: 1, min: 5, max: 200, }, reverse: { - type: 'boolean' as const, + type: 'boolean', default: false, }, showHeader: { - type: 'boolean' as const, + type: 'boolean', default: false, }, transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index 3fe8cfa7e6..8e5dc9e8d3 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -22,7 +22,7 @@ import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -32,15 +32,15 @@ const name = 'slideshow'; const widgetPropsDef = { height: { - type: 'number' as const, + type: 'number', default: 300, }, folderId: { - type: 'string' as const, - default: null, + type: 'string', + default: null as string | null, hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -59,7 +59,7 @@ const slideA = useTemplateRef('slideA'); const slideB = useTemplateRef('slideB'); const change = () => { - if (images.value.length === 0) return; + if (images.value.length === 0 || slideA.value == null || slideB.value == null) return; const index = Math.floor(Math.random() * images.value.length); const img = `url(${ images.value[index].url })`; @@ -73,11 +73,12 @@ const change = () => { slideA.value.style.backgroundImage = img; - slideB.value.classList.remove('anime'); + slideB.value!.classList.remove('anime'); }, 1000); }; const fetch = () => { + if (slideA.value == null || slideB.value == null) return; fetching.value = true; misskeyApi('drive/files', { @@ -87,8 +88,8 @@ const fetch = () => { }).then(res => { images.value = res; fetching.value = false; - slideA.value.style.backgroundImage = ''; - slideB.value.style.backgroundImage = ''; + slideA.value!.style.backgroundImage = ''; + slideB.value!.style.backgroundImage = ''; change(); }); }; diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index 9cbeb9cf2e..6c775fd98c 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #header> <button class="_button" @click="choose"> - <span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.ts._timelines[widgetProps.src] }}</span> + <span>{{ headerTitle }}</span> <i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i> </button> </template> @@ -25,51 +25,59 @@ SPDX-License-Identifier: AGPL-3.0-only <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> </div> <div v-else> - <MkStreamingNotesTimeline :key="widgetProps.src === 'list' ? `list:${widgetProps.list.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna.id}` : widgetProps.src" :src="widgetProps.src" :list="widgetProps.list ? widgetProps.list.id : null" :antenna="widgetProps.antenna ? widgetProps.antenna.id : null"/> + <MkStreamingNotesTimeline + :key="widgetProps.src === 'list' ? `list:${widgetProps.list?.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna?.id}` : widgetProps.src" + :src="widgetProps.src" + :list="widgetProps.list ? widgetProps.list.id : undefined" + :antenna="widgetProps.antenna ? widgetProps.antenna.id : undefined" + /> </div> </MkContainer> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; +import * as Misskey from 'misskey-js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.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 } from '@/timelines.js'; +import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; import type { MenuItem } from '@/types/menu.js'; const name = 'timeline'; +type TlSrc = typeof basicTimelineTypes[number] | 'list' | 'antenna'; + const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, height: { - type: 'number' as const, + type: 'number', default: 300, }, src: { - type: 'string' as const, - default: 'home', + type: 'string', + default: 'home' as TlSrc, hidden: true, }, antenna: { - type: 'object' as const, - default: null, + type: 'object', + default: null as Misskey.entities.Antenna | null, hidden: true, }, list: { - type: 'object' as const, - default: null, + type: 'object', + default: null as Misskey.entities.UserList | null, hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -84,12 +92,22 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name, const menuOpened = ref(false); -const setSrc = (src) => { +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; + } else { + return i18n.ts._timelines[widgetProps.src]; + } +}); + +const setSrc = (src: TlSrc) => { widgetProps.src = src; save(); }; -const choose = async (ev) => { +const choose = async (ev: MouseEvent) => { 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 db09031c33..dcb900b0c9 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApiGet } from '@/utility/misskey-api.js'; @@ -40,10 +40,10 @@ const name = 'hashtags'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue index f51ef12a2a..226a4c73aa 100644 --- a/packages/frontend/src/widgets/WidgetUnixClock.vue +++ b/packages/frontend/src/widgets/WidgetUnixClock.vue @@ -19,29 +19,29 @@ SPDX-License-Identifier: AGPL-3.0-only import { onUnmounted, ref, watch } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; const name = 'unixClock'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, fontSize: { - type: 'number' as const, + type: 'number', default: 1.5, step: 0.1, }, showMs: { - type: 'boolean' as const, + type: 'boolean', default: true, }, showLabel: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -54,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -let intervalId; +let intervalId: number | null = null; const ss = ref(''); const ms = ref(''); const showColon = ref(false); @@ -84,7 +84,10 @@ watch(() => widgetProps.showMs, () => { }, { immediate: true }); onUnmounted(() => { - window.clearInterval(intervalId); + if (intervalId) { + window.clearInterval(intervalId); + intervalId = null; + } }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index eb86732817..d87ea5ade2 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -28,7 +28,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -40,15 +40,15 @@ const name = 'userList'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, listId: { - type: 'string' as const, - default: null, + type: 'string', + default: null as string | null, hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -61,7 +61,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name, emit, ); -const list = ref<Misskey.entities.UserList>(); +const list = ref<Misskey.entities.UserList | null>(null); const users = ref<Misskey.entities.UserDetailed[]>([]); const fetching = ref(true); @@ -74,7 +74,7 @@ async function chooseList() { })), default: widgetProps.listId, }); - if (canceled) return; + if (canceled || list == null) return; widgetProps.listId = list.id; save(); @@ -92,7 +92,7 @@ const fetch = () => { }).then(_list => { list.value = _list; misskeyApi('users/show', { - userIds: list.value.userIds, + userIds: list.value.userIds ?? [], }).then(_users => { users.value = _users; fetching.value = false; diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index d8a876f936..fe86b73f99 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -80,7 +80,7 @@ import * as Misskey from 'misskey-js'; import { genId } from '@/utility/id.js'; const props = defineProps<{ - connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, + connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue index ba98a926ff..99f502d94c 100644 --- a/packages/frontend/src/widgets/server-metric/cpu.vue +++ b/packages/frontend/src/widgets/server-metric/cpu.vue @@ -20,7 +20,7 @@ import * as Misskey from 'misskey-js'; import XPie from './pie.vue'; const props = defineProps<{ - connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, + connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index 9026fefb20..f52b6fd12e 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/> <XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/> <XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/> - <XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/> + <XDisk v-else-if="widgetProps.view === 4" :meta="meta"/> </div> </MkContainer> </template> @@ -30,7 +30,7 @@ import XCpu from './cpu.vue'; import XMemory from './mem.vue'; import XDisk from './disk.vue'; import MkContainer from '@/components/MkContainer.vue'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { misskeyApiGet } from '@/utility/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; @@ -39,19 +39,19 @@ const name = 'serverMetric'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, view: { - type: 'number' as const, + type: 'number', default: 0, hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue index ff4c6a44b4..089281b2ef 100644 --- a/packages/frontend/src/widgets/server-metric/mem.vue +++ b/packages/frontend/src/widgets/server-metric/mem.vue @@ -22,7 +22,7 @@ import XPie from './pie.vue'; import bytes from '@/filters/bytes.js'; const props = defineProps<{ - connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, + connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue index 5e08a47100..38acd633a6 100644 --- a/packages/frontend/src/widgets/server-metric/net.vue +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -55,7 +55,7 @@ import bytes from '@/filters/bytes.js'; import { genId } from '@/utility/id.js'; const props = defineProps<{ - connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, + connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts index de4c369cbb..c5ca7ac26c 100644 --- a/packages/frontend/src/widgets/widget.ts +++ b/packages/frontend/src/widgets/widget.ts @@ -4,8 +4,9 @@ */ import { reactive, watch } from 'vue'; +import type { Reactive } from 'vue'; import { throttle } from 'throttle-debounce'; -import type { Form, GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import { deepClone } from '@/utility/clone.js'; @@ -28,17 +29,17 @@ export type WidgetComponentExpose = { configure: () => void; }; -export const useWidgetPropsManager = <F extends Form & Record<string, { default: any; }>>( +export const useWidgetPropsManager = <F extends FormWithDefault>( name: string, propsDef: F, props: Readonly<WidgetComponentProps<GetFormResultType<F>>>, emit: WidgetComponentEmits<GetFormResultType<F>>, ): { - widgetProps: GetFormResultType<F>; + widgetProps: Reactive<GetFormResultType<F>>; save: () => void; configure: () => void; } => { - const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {}); + const widgetProps = reactive<GetFormResultType<F>>((props.widget ? deepClone(props.widget.data) : {}) as GetFormResultType<F>); const mergeProps = () => { for (const prop of Object.keys(propsDef)) { @@ -47,12 +48,13 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default: } } }; + watch(widgetProps, () => { mergeProps(); }, { deep: true, immediate: true }); const save = throttle(3000, () => { - emit('updateProps', widgetProps); + emit('updateProps', widgetProps as GetFormResultType<F>); }); const configure = async () => { |