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>
This commit is contained in:
かっこかり 2025-07-06 19:36:11 +09:00 committed by GitHub
parent c2a01551a7
commit a8abb03d17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 344 additions and 239 deletions

View File

@ -13,7 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js'; import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.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 MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
@ -837,7 +837,7 @@ export function launchUploader(
options?: { options?: {
folderId?: string | null; folderId?: string | null;
multiple?: boolean; multiple?: boolean;
features?: UploaderDialogFeatures; features?: UploaderFeatures;
}, },
): Promise<Misskey.entities.DriveFile[]> { ): Promise<Misskey.entities.DriveFile[]> {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {

View File

@ -76,7 +76,8 @@ watch(() => props.clipId, async () => {
clip.value = await misskeyApi('clips/show', { clip.value = await misskeyApi('clips/show', {
clipId: props.clipId, clipId: props.clipId,
}); });
favorited.value = clip.value.isFavorited;
favorited.value = clip.value!.isFavorited ?? false;
}, { }, {
immediate: true, immediate: true,
}); });
@ -108,6 +109,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
icon: 'ti ti-pencil', icon: 'ti ti-pencil',
text: i18n.ts.edit, text: i18n.ts.edit,
handler: async (): Promise<void> => { handler: async (): Promise<void> => {
if (clip.value == null) return;
const { canceled, result } = await os.form(clip.value.name, { const { canceled, result } = await os.form(clip.value.name, {
name: { name: {
type: 'string', type: 'string',
@ -128,6 +131,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
default: clip.value.isPublic, default: clip.value.isPublic,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('clips/update', { os.apiWithDialog('clips/update', {
@ -178,6 +182,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
text: i18n.ts.delete, text: i18n.ts.delete,
danger: true, danger: true,
handler: async (): Promise<void> => { handler: async (): Promise<void> => {
if (clip.value == null) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }), text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),

View File

@ -64,6 +64,7 @@ async function create() {
default: false, default: false,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('clips/create', result); os.apiWithDialog('clips/create', result);

View File

@ -79,7 +79,9 @@ async function createKey() {
default: scope.value.join('/'), default: scope.value.join('/'),
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('i/registry/set', { os.apiWithDialog('i/registry/set', {
scope: result.scope.split('/'), scope: result.scope.split('/'),
key: result.key, key: result.key,

View File

@ -56,7 +56,9 @@ async function createKey() {
label: i18n.ts._registry.scope, label: i18n.ts._registry.scope,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('i/registry/set', { os.apiWithDialog('i/registry/set', {
scope: result.scope.split('/'), scope: result.scope.split('/'),
key: result.key, key: result.key,

View File

@ -14,12 +14,13 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import type { FormWithDefault } from '@/utility/form.js';
export type Plugin = { export type Plugin = {
installId: string; installId: string;
name: string; name: string;
active: boolean; active: boolean;
config?: Record<string, { default: any }>; config?: FormWithDefault;
configData: Record<string, any>; configData: Record<string, any>;
src: string | null; src: string | null;
version: string; version: string;
@ -240,7 +241,7 @@ async function launchPlugin(id: Plugin['installId']): Promise<void> {
pluginLogs.value.set(plugin.installId, []); pluginLogs.value.set(plugin.installId, []);
function systemLog(message: string, isError = false): void { function systemLog(message: string, isError = false): void {
pluginLogs.value.get(plugin.installId)?.push({ pluginLogs.value.get(plugin!.installId)?.push({
at: Date.now(), at: Date.now(),
isSystem: true, isSystem: true,
message, message,

View File

@ -29,7 +29,7 @@ export const store = markRaw(new Pizzax('base', {
}, },
memo: { memo: {
where: 'account', where: 'account',
default: null, default: null as string | null,
}, },
reactionAcceptance: { reactionAcceptance: {
where: 'account', where: 'account',

View File

@ -29,7 +29,8 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
label: i18n.ts.sound, label: i18n.ts.sound,
default: soundSetting.value.type ?? 'none', default: soundSetting.value.type ?? 'none',
enum: soundsTypes.map(f => ({ enum: soundsTypes.map(f => ({
value: f ?? 'none', label: getSoundTypeName(f), value: f ?? 'none' as Exclude<SoundType, null> | 'none',
label: getSoundTypeName(f),
})), })),
}, },
soundFile: { soundFile: {
@ -81,16 +82,17 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
}, },
}, },
}); });
if (canceled) return; if (canceled) return;
const res = buildSoundStore(result); const res = buildSoundStore(result);
if (res) soundSetting.value = res; if (res) soundSetting.value = res;
function buildSoundStore(result: any): SoundStore | null { function buildSoundStore(r: NonNullable<typeof result>): SoundStore | null {
const type = (result.type === 'none' ? null : result.type) as SoundType; const type = (r.type === 'none' ? null : r.type);
const volume = result.volume as number; const volume = r.volume;
const fileId = result.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined); const fileId = r.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined);
const fileUrl = result.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined); const fileUrl = r.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined);
if (type === '_driveFile_') { if (type === '_driveFile_') {
if (!fileUrl || !fileId) { if (!fileUrl || !fileId) {

View File

@ -5,55 +5,59 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
type EnumItem = string | { export type EnumItem = string | {
label: string; label: string;
value: string; value: unknown;
}; };
type Hidden = boolean | ((v: any) => boolean); type Hidden = boolean | ((v: any) => boolean);
export type FormItem = { interface FormItemBase {
label?: string; label?: string;
hidden?: Hidden;
}
export interface StringFormItem extends FormItemBase {
type: 'string'; type: 'string';
default?: string | null; default?: string | null;
description?: string; description?: string;
required?: boolean; required?: boolean;
hidden?: Hidden;
multiline?: boolean; multiline?: boolean;
treatAsMfm?: boolean; treatAsMfm?: boolean;
} | { }
label?: string;
export interface NumberFormItem extends FormItemBase {
type: 'number'; type: 'number';
default?: number | null; default?: number | null;
description?: string; description?: string;
required?: boolean; required?: boolean;
hidden?: Hidden;
step?: number; step?: number;
} | { }
label?: string;
export interface BooleanFormItem extends FormItemBase {
type: 'boolean'; type: 'boolean';
default?: boolean | null; default?: boolean | null;
description?: string; description?: string;
hidden?: Hidden; }
} | {
label?: string; export interface EnumFormItem extends FormItemBase {
type: 'enum'; type: 'enum';
default?: string | null; default?: string | null;
required?: boolean; required?: boolean;
hidden?: Hidden;
enum: EnumItem[]; enum: EnumItem[];
} | { }
label?: string;
export interface RadioFormItem extends FormItemBase {
type: 'radio'; type: 'radio';
default?: unknown | null; default?: unknown | null;
required?: boolean; required?: boolean;
hidden?: Hidden;
options: { options: {
label: string; label: string;
value: unknown; value: unknown;
}[]; }[];
} | { }
label?: string;
export interface RangeFormItem extends FormItemBase {
type: 'range'; type: 'range';
default?: number | null; default?: number | null;
description?: string; description?: string;
@ -62,42 +66,80 @@ export type FormItem = {
min: number; min: number;
max: number; max: number;
textConverter?: (value: number) => string; textConverter?: (value: number) => string;
hidden?: Hidden; }
} | {
label?: string; export interface ObjectFormItem extends FormItemBase {
type: 'object'; type: 'object';
default?: Record<string, unknown> | null; default?: Record<string, unknown> | null;
hidden: Hidden; }
} | {
label?: string; export interface ArrayFormItem extends FormItemBase {
type: 'array'; type: 'array';
default?: unknown[] | null; default?: unknown[] | null;
hidden: Hidden; }
} | {
export interface ButtonFormItem extends FormItemBase {
type: 'button'; type: 'button';
content?: string; content?: string;
hidden?: Hidden;
action: (ev: MouseEvent, v: any) => void; action: (ev: MouseEvent, v: any) => void;
} | { }
export interface DriveFileFormItem extends FormItemBase {
type: 'drive-file'; type: 'drive-file';
defaultFileId?: string | null; defaultFileId?: string | null;
hidden?: Hidden;
validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>; 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 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> = type GetItemType<Item extends FormItem> =
Item['type'] extends 'string' ? string : Item extends StringFormItem
Item['type'] extends 'number' ? number : ? NonNullableIfRequired<InferDefault<Item, string>, Item>
Item['type'] extends 'boolean' ? boolean : : Item extends NumberFormItem
Item['type'] extends 'radio' ? unknown : ? NonNullableIfRequired<InferDefault<Item, number>, Item>
Item['type'] extends 'range' ? number : : Item extends BooleanFormItem
Item['type'] extends 'enum' ? string : ? boolean
Item['type'] extends 'array' ? unknown[] : : Item extends RadioFormItem
Item['type'] extends 'object' ? Record<string, unknown> : ? GetRadioItemType<Item>
Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined : : Item extends RangeFormItem
never; ? 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> = { export type GetFormResultType<F extends Form> = {
[P in keyof F]: GetItemType<F[P]>; [P in keyof F]: GetItemType<F[P]>;

View File

@ -101,7 +101,7 @@ export async function getNoteClipMenu(props: {
const { canceled, result } = await os.form(i18n.ts.createNewClip, { const { canceled, result } = await os.form(i18n.ts.createNewClip, {
name: { name: {
type: 'string', type: 'string',
default: null, default: null as string | null,
label: i18n.ts.name, label: i18n.ts.name,
}, },
description: { description: {

View File

@ -132,6 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
const userDetailed = await misskeyApi('users/show', { const userDetailed = await misskeyApi('users/show', {
userId: user.id, userId: user.id,
}); });
const { canceled, result } = await os.form(i18n.ts.editMemo, { const { canceled, result } = await os.form(i18n.ts.editMemo, {
memo: { memo: {
type: 'string', type: 'string',
@ -141,6 +142,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
default: userDetailed.memo, default: userDetailed.memo,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('users/update-memo', { os.apiWithDialog('users/update-memo', {

View File

@ -25,29 +25,31 @@ import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
import XCalendar from './WidgetActivity.calendar.vue'; import XCalendar from './WidgetActivity.calendar.vue';
import XChart from './WidgetActivity.chart.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 { misskeyApiGet } from '@/utility/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import { $i } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const $i = ensureSignin();
const name = 'activity'; const name = 'activity';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
view: { view: {
type: 'number' as const, type: 'number',
default: 0, default: 0,
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } 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 name = 'ai';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -42,6 +42,8 @@ const touched = () => {
}; };
const onMousemove = (ev: MouseEvent) => { const onMousemove = (ev: MouseEvent) => {
if (!live2d.value || !live2d.value.contentWindow) return;
const iframeRect = live2d.value.getBoundingClientRect(); const iframeRect = live2d.value.getBoundingClientRect();
live2d.value.contentWindow.postMessage({ live2d.value.contentWindow.postMessage({
type: 'moveCursor', type: 'moveCursor',

View File

@ -23,7 +23,7 @@ import { ref } from 'vue';
import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 * as os from '@/os.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
@ -35,16 +35,16 @@ const name = 'aiscript';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
script: { script: {
type: 'string' as const, type: 'string',
multiline: true, multiline: true,
default: '(1 + 1)', default: '(1 + 1)',
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -106,7 +106,7 @@ const run = async () => {
} catch (err) { } catch (err) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err, text: err instanceof Error ? err.message : String(err),
}); });
} }
}; };

View File

@ -18,7 +18,7 @@ import type { Ref } from 'vue';
import { Interpreter, Parser } from '@syuilo/aiscript'; import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 * as os from '@/os.js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
@ -31,15 +31,15 @@ const name = 'aiscriptApp';
const widgetPropsDef = { const widgetPropsDef = {
script: { script: {
type: 'string' as const, type: 'string',
multiline: true, multiline: true,
default: '', default: '',
}, },
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -92,7 +92,7 @@ async function run() {
os.alert({ os.alert({
type: 'error', type: 'error',
title: 'AiScript Error', title: 'AiScript Error',
text: err.message, text: err instanceof Error ? err.message : String(err),
}); });
} }
} }

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.bdayFRoot"> <div :class="$style.bdayFRoot">
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
<div v-else-if="users.length > 0" :class="$style.bdayFGrid"> <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>
<div v-else :class="$style.bdayFFallback"> <div v-else :class="$style.bdayFFallback">
<MkResult type="empty"/> <MkResult type="empty"/>
@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -37,10 +37,10 @@ const name = i18n.ts._widgets.birthdayFollowings;
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { Interpreter, Parser } from '@syuilo/aiscript'; import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 * as os from '@/os.js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
@ -25,19 +25,19 @@ const name = 'button';
const widgetPropsDef = { const widgetPropsDef = {
label: { label: {
type: 'string' as const, type: 'string',
default: 'BUTTON', default: 'BUTTON',
}, },
colored: { colored: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
script: { script: {
type: 'string' as const, type: 'string',
multiline: true, multiline: true,
default: 'Mk:dialog("hello" "world")', default: 'Mk:dialog("hello" "world")',
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -81,7 +81,7 @@ const run = async () => {
} catch (err) { } catch (err) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err, text: err instanceof Error ? err.message : String(err),
}); });
} }
}; };

View File

@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue'; import { ref } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 { i18n } from '@/i18n.js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
@ -49,10 +49,10 @@ const name = 'calendar';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { } from 'vue'; import { } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkChatHistories from '@/components/MkChatHistories.vue'; import MkChatHistories from '@/components/MkChatHistories.vue';
@ -28,10 +28,10 @@ const name = 'chat';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import MkClickerGame from '@/components/MkClickerGame.vue'; import MkClickerGame from '@/components/MkClickerGame.vue';
@ -22,10 +22,10 @@ const name = 'clicker';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed } from 'vue'; import { computed } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import MkAnalogClock from '@/components/MkAnalogClock.vue'; import MkAnalogClock from '@/components/MkAnalogClock.vue';
import MkDigitalClock from '@/components/MkDigitalClock.vue'; import MkDigitalClock from '@/components/MkDigitalClock.vue';
@ -43,76 +43,92 @@ const name = 'clock';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
size: { size: {
type: 'radio' as const, type: 'radio',
default: 'medium', default: 'medium',
options: [{ 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: { thickness: {
type: 'radio' as const, type: 'radio',
default: 0.2, default: 0.2,
options: [{ 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: { graduations: {
type: 'radio' as const, type: 'radio',
default: 'numbers', default: 'numbers',
options: [{ 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: { fadeGraduations: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
sAnimation: { sAnimation: {
type: 'radio' as const, type: 'radio',
default: 'elastic', default: 'elastic',
options: [{ 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: { twentyFour: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
label: { label: {
type: 'radio' as const, type: 'radio',
default: 'none', default: 'none',
options: [{ 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: { timezone: {
type: 'enum' as const, type: 'enum',
default: null, default: null,
enum: [...timezones.map((tz) => ({ enum: [...timezones.map((tz) => ({
label: tz.name, label: tz.name,
@ -122,7 +138,7 @@ const widgetPropsDef = {
value: null, value: null,
}], }],
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed } from 'vue'; import { computed } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 { timezones } from '@/utility/timezones.js';
import MkDigitalClock from '@/components/MkDigitalClock.vue'; import MkDigitalClock from '@/components/MkDigitalClock.vue';
@ -25,24 +25,24 @@ const name = 'digitalClock';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
fontSize: { fontSize: {
type: 'number' as const, type: 'number',
default: 1.5, default: 1.5,
step: 0.1, step: 0.1,
}, },
showMs: { showMs: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
showLabel: { showLabel: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
timezone: { timezone: {
type: 'enum' as const, type: 'enum',
default: null, default: null,
enum: [...timezones.map((tz) => ({ enum: [...timezones.map((tz) => ({
label: tz.name, label: tz.name,
@ -52,7 +52,7 @@ const widgetPropsDef = {
value: null, value: null,
}], }],
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <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 #icon><i class="ti ti-whirl"></i></template>
<template #header>{{ i18n.ts._widgets.federation }}</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 { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import MkMiniChart from '@/components/MkMiniChart.vue'; import MkMiniChart from '@/components/MkMiniChart.vue';
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
@ -42,10 +42,10 @@ const name = 'federation';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -23,7 +23,7 @@ import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import MkTagCloud from '@/components/MkTagCloud.vue'; import MkTagCloud from '@/components/MkTagCloud.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -34,10 +34,10 @@ const name = 'instanceCloud';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_panel"> <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"> <div :class="$style.iconContainer">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/> <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
</div> </div>
@ -22,14 +22,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 { host } from '@@/js/config.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
const name = 'instanceInfo'; const name = 'instanceInfo';
const widgetPropsDef = { const widgetPropsDef = {
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onUnmounted, reactive, ref } from 'vue'; import { onUnmounted, reactive, ref } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 { useStream } from '@/stream.js';
import kmg from '@/filters/kmg.js'; import kmg from '@/filters/kmg.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
@ -66,14 +66,14 @@ const name = 'jobQueue';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
sound: { sound: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>{{ i18n.ts._widgets.memo }}</template> <template #header>{{ i18n.ts._widgets.memo }}</template>
<div :class="$style.root"> <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> <button :class="$style.save" :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button>
</div> </div>
</MkContainer> </MkContainer>
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -28,14 +28,14 @@ const name = 'memo';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
height: { height: {
type: 'number' as const, type: 'number',
default: 100, default: 100,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -17,9 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import type { notificationTypes as notificationTypes_typeReferenceOnly } from '@@/js/const.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue'; import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -29,19 +30,19 @@ const name = 'notifications';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
height: { height: {
type: 'number' as const, type: 'number',
default: 300, default: 300,
}, },
excludeTypes: { excludeTypes: {
type: 'array' as const, type: 'array',
hidden: true, hidden: true,
default: [], default: [] as (typeof notificationTypes_typeReferenceOnly[number])[],
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue'; import { ref } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApiGet } from '@/utility/misskey-api.js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
@ -27,10 +27,10 @@ const name = 'onlineUsers';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -26,7 +26,7 @@ import { onUnmounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 { useStream } from '@/stream.js';
import { getStaticImageUrl } from '@/utility/media-proxy.js'; import { getStaticImageUrl } from '@/utility/media-proxy.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
@ -38,14 +38,14 @@ const name = 'photos';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -11,13 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only
import { } from 'vue'; import { } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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'; import MkPostForm from '@/components/MkPostForm.vue';
const name = 'postForm'; const name = 'postForm';
const widgetPropsDef = { const widgetPropsDef = {
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_panel"> <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"> <div :class="$style.avatarContainer">
<MkAvatar :class="$style.avatar" :user="$i"/> <MkAvatar :class="$style.avatar" :user="$i"/>
</div> </div>
@ -24,14 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 { $i } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
const $i = ensureSignin();
const name = 'profile'; const name = 'profile';
const widgetPropsDef = { const widgetPropsDef = {
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -26,7 +26,7 @@ import { url as base } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -34,22 +34,22 @@ const name = 'rss';
const widgetPropsDef = { const widgetPropsDef = {
url: { url: {
type: 'string' as const, type: 'string',
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
}, },
refreshIntervalSec: { refreshIntervalSec: {
type: 'number' as const, type: 'number',
default: 60, default: 60,
}, },
maxEntries: { maxEntries: {
type: 'number' as const, type: 'number',
default: 15, default: 15,
}, },
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -32,7 +32,7 @@ import * as Misskey from 'misskey-js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import MarqueeText from '@/components/MkMarqueeText.vue'; 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 MkContainer from '@/components/MkContainer.vue';
import { shuffle } from '@/utility/shuffle.js'; import { shuffle } from '@/utility/shuffle.js';
import { url as base } from '@@/js/config.js'; import { url as base } from '@@/js/config.js';
@ -42,41 +42,41 @@ const name = 'rssTicker';
const widgetPropsDef = { const widgetPropsDef = {
url: { url: {
type: 'string' as const, type: 'string',
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
}, },
shuffle: { shuffle: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
refreshIntervalSec: { refreshIntervalSec: {
type: 'number' as const, type: 'number',
default: 60, default: 60,
}, },
maxEntries: { maxEntries: {
type: 'number' as const, type: 'number',
default: 15, default: 15,
}, },
duration: { duration: {
type: 'range' as const, type: 'range',
default: 70, default: 70,
step: 1, step: 1,
min: 5, min: 5,
max: 200, max: 200,
}, },
reverse: { reverse: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -22,7 +22,7 @@ import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -32,15 +32,15 @@ const name = 'slideshow';
const widgetPropsDef = { const widgetPropsDef = {
height: { height: {
type: 'number' as const, type: 'number',
default: 300, default: 300,
}, },
folderId: { folderId: {
type: 'string' as const, type: 'string',
default: null, default: null as string | null,
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -59,7 +59,7 @@ const slideA = useTemplateRef('slideA');
const slideB = useTemplateRef('slideB'); const slideB = useTemplateRef('slideB');
const change = () => { 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 index = Math.floor(Math.random() * images.value.length);
const img = `url(${ images.value[index].url })`; const img = `url(${ images.value[index].url })`;
@ -73,11 +73,12 @@ const change = () => {
slideA.value.style.backgroundImage = img; slideA.value.style.backgroundImage = img;
slideB.value.classList.remove('anime'); slideB.value!.classList.remove('anime');
}, 1000); }, 1000);
}; };
const fetch = () => { const fetch = () => {
if (slideA.value == null || slideB.value == null) return;
fetching.value = true; fetching.value = true;
misskeyApi('drive/files', { misskeyApi('drive/files', {
@ -87,8 +88,8 @@ const fetch = () => {
}).then(res => { }).then(res => {
images.value = res; images.value = res;
fetching.value = false; fetching.value = false;
slideA.value.style.backgroundImage = ''; slideA.value!.style.backgroundImage = '';
slideB.value.style.backgroundImage = ''; slideB.value!.style.backgroundImage = '';
change(); change();
}); });
}; };

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<template #header> <template #header>
<button class="_button" @click="choose"> <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> <i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i>
</button> </button>
</template> </template>
@ -25,51 +25,59 @@ SPDX-License-Identifier: AGPL-3.0-only
<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
</div> </div>
<div v-else> <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> </div>
</MkContainer> </MkContainer>
</template> </template>
<script lang="ts" setup> <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 { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
import { i18n } from '@/i18n.js'; 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'; import type { MenuItem } from '@/types/menu.js';
const name = 'timeline'; const name = 'timeline';
type TlSrc = typeof basicTimelineTypes[number] | 'list' | 'antenna';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
height: { height: {
type: 'number' as const, type: 'number',
default: 300, default: 300,
}, },
src: { src: {
type: 'string' as const, type: 'string',
default: 'home', default: 'home' as TlSrc,
hidden: true, hidden: true,
}, },
antenna: { antenna: {
type: 'object' as const, type: 'object',
default: null, default: null as Misskey.entities.Antenna | null,
hidden: true, hidden: true,
}, },
list: { list: {
type: 'object' as const, type: 'object',
default: null, default: null as Misskey.entities.UserList | null,
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -84,12 +92,22 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
const menuOpened = ref(false); 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; widgetProps.src = src;
save(); save();
}; };
const choose = async (ev) => { const choose = async (ev: MouseEvent) => {
menuOpened.value = true; menuOpened.value = true;
const [antennas, lists] = await Promise.all([ const [antennas, lists] = await Promise.all([
misskeyApi('antennas/list'), misskeyApi('antennas/list'),

View File

@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import MkMiniChart from '@/components/MkMiniChart.vue'; import MkMiniChart from '@/components/MkMiniChart.vue';
import { misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApiGet } from '@/utility/misskey-api.js';
@ -40,10 +40,10 @@ const name = 'hashtags';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -19,29 +19,29 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onUnmounted, ref, watch } from 'vue'; import { onUnmounted, ref, watch } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 name = 'unixClock';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
fontSize: { fontSize: {
type: 'number' as const, type: 'number',
default: 1.5, default: 1.5,
step: 0.1, step: 0.1,
}, },
showMs: { showMs: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
showLabel: { showLabel: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -54,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit, emit,
); );
let intervalId; let intervalId: number | null = null;
const ss = ref(''); const ss = ref('');
const ms = ref(''); const ms = ref('');
const showColon = ref(false); const showColon = ref(false);
@ -84,7 +84,10 @@ watch(() => widgetProps.showMs, () => {
}, { immediate: true }); }, { immediate: true });
onUnmounted(() => { onUnmounted(() => {
window.clearInterval(intervalId); if (intervalId) {
window.clearInterval(intervalId);
intervalId = null;
}
}); });
defineExpose<WidgetComponentExpose>({ defineExpose<WidgetComponentExpose>({

View File

@ -28,7 +28,7 @@ import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } 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 MkContainer from '@/components/MkContainer.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
@ -40,15 +40,15 @@ const name = 'userList';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
listId: { listId: {
type: 'string' as const, type: 'string',
default: null, default: null as string | null,
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -61,7 +61,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
emit, emit,
); );
const list = ref<Misskey.entities.UserList>(); const list = ref<Misskey.entities.UserList | null>(null);
const users = ref<Misskey.entities.UserDetailed[]>([]); const users = ref<Misskey.entities.UserDetailed[]>([]);
const fetching = ref(true); const fetching = ref(true);
@ -74,7 +74,7 @@ async function chooseList() {
})), })),
default: widgetProps.listId, default: widgetProps.listId,
}); });
if (canceled) return; if (canceled || list == null) return;
widgetProps.listId = list.id; widgetProps.listId = list.id;
save(); save();
@ -92,7 +92,7 @@ const fetch = () => {
}).then(_list => { }).then(_list => {
list.value = _list; list.value = _list;
misskeyApi('users/show', { misskeyApi('users/show', {
userIds: list.value.userIds, userIds: list.value.userIds ?? [],
}).then(_users => { }).then(_users => {
users.value = _users; users.value = _users;
fetching.value = false; fetching.value = false;

View File

@ -80,7 +80,7 @@ import * as Misskey from 'misskey-js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
const props = defineProps<{ const props = defineProps<{
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse meta: Misskey.entities.ServerInfoResponse
}>(); }>();

View File

@ -20,7 +20,7 @@ import * as Misskey from 'misskey-js';
import XPie from './pie.vue'; import XPie from './pie.vue';
const props = defineProps<{ const props = defineProps<{
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse meta: Misskey.entities.ServerInfoResponse
}>(); }>();

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/> <XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
<XCpu v-else-if="widgetProps.view === 2" :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"/> <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> </div>
</MkContainer> </MkContainer>
</template> </template>
@ -30,7 +30,7 @@ import XCpu from './cpu.vue';
import XMemory from './mem.vue'; import XMemory from './mem.vue';
import XDisk from './disk.vue'; import XDisk from './disk.vue';
import MkContainer from '@/components/MkContainer.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 { misskeyApiGet } from '@/utility/misskey-api.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -39,19 +39,19 @@ const name = 'serverMetric';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
view: { view: {
type: 'number' as const, type: 'number',
default: 0, default: 0,
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View File

@ -22,7 +22,7 @@ import XPie from './pie.vue';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
const props = defineProps<{ const props = defineProps<{
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse meta: Misskey.entities.ServerInfoResponse
}>(); }>();

View File

@ -55,7 +55,7 @@ import bytes from '@/filters/bytes.js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
const props = defineProps<{ const props = defineProps<{
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse meta: Misskey.entities.ServerInfoResponse
}>(); }>();

View File

@ -4,8 +4,9 @@
*/ */
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import type { Reactive } from 'vue';
import { throttle } from 'throttle-debounce'; 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 * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
@ -28,17 +29,17 @@ export type WidgetComponentExpose = {
configure: () => void; configure: () => void;
}; };
export const useWidgetPropsManager = <F extends Form & Record<string, { default: any; }>>( export const useWidgetPropsManager = <F extends FormWithDefault>(
name: string, name: string,
propsDef: F, propsDef: F,
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>, props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
emit: WidgetComponentEmits<GetFormResultType<F>>, emit: WidgetComponentEmits<GetFormResultType<F>>,
): { ): {
widgetProps: GetFormResultType<F>; widgetProps: Reactive<GetFormResultType<F>>;
save: () => void; save: () => void;
configure: () => 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 = () => { const mergeProps = () => {
for (const prop of Object.keys(propsDef)) { for (const prop of Object.keys(propsDef)) {
@ -47,12 +48,13 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
} }
} }
}; };
watch(widgetProps, () => { watch(widgetProps, () => {
mergeProps(); mergeProps();
}, { deep: true, immediate: true }); }, { deep: true, immediate: true });
const save = throttle(3000, () => { const save = throttle(3000, () => {
emit('updateProps', widgetProps); emit('updateProps', widgetProps as GetFormResultType<F>);
}); });
const configure = async () => { const configure = async () => {