refactor: Widgetのcomposition api移行 (#8125)
* wip * wip * wip * wip * wip * wip * fix
This commit is contained in:
		
							parent
							
								
									faef125b74
								
							
						
					
					
						commit
						0bbde336b3
					
				
					 23 changed files with 1389 additions and 1221 deletions
				
			
		|  | @ -10,7 +10,7 @@ | |||
| 			<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton> | ||||
| 		</header> | ||||
| 		<XDraggable | ||||
| 			v-model="_widgets" | ||||
| 			v-model="widgets_" | ||||
| 			item-key="id" | ||||
| 			animation="150" | ||||
| 		> | ||||
|  | @ -18,7 +18,7 @@ | |||
| 				<div class="customize-container"> | ||||
| 					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button> | ||||
| 					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button> | ||||
| 					<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" @updateProps="updateWidget(element.id, $event)"/> | ||||
| 					<component :ref="el => widgetRefs[element.id] = el" :is="`mkw-${element.name}`" :widget="element" @updateProps="updateWidget(element.id, $event)"/> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 		</XDraggable> | ||||
|  | @ -28,7 +28,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | ||||
| import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
|  | @ -54,50 +54,47 @@ export default defineComponent({ | |||
| 
 | ||||
| 	emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			widgetAdderSelected: null, | ||||
| 			widgetDefs, | ||||
| 			settings: {}, | ||||
| 	setup(props, context) { | ||||
| 		const widgetRefs = reactive({}); | ||||
| 		const configWidget = (id: string) => { | ||||
| 			widgetRefs[id].configure(); | ||||
| 		}; | ||||
| 	}, | ||||
| 		const widgetAdderSelected = ref(null); | ||||
| 		const addWidget = () => { | ||||
| 			if (widgetAdderSelected.value == null) return; | ||||
| 
 | ||||
| 	computed: { | ||||
| 		_widgets: { | ||||
| 			get() { | ||||
| 				return this.widgets; | ||||
| 			}, | ||||
| 			set(value) { | ||||
| 				this.$emit('updateWidgets', value); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		configWidget(id) { | ||||
| 			this.settings[id](); | ||||
| 		}, | ||||
| 
 | ||||
| 		addWidget() { | ||||
| 			if (this.widgetAdderSelected == null) return; | ||||
| 
 | ||||
| 			this.$emit('addWidget', { | ||||
| 				name: this.widgetAdderSelected, | ||||
| 			context.emit('addWidget', { | ||||
| 				name: widgetAdderSelected.value, | ||||
| 				id: uuid(), | ||||
| 				data: {} | ||||
| 				data: {}, | ||||
| 			}); | ||||
| 
 | ||||
| 			this.widgetAdderSelected = null; | ||||
| 		}, | ||||
| 			widgetAdderSelected.value = null; | ||||
| 		}; | ||||
| 		const removeWidget = (widget) => { | ||||
| 			context.emit('removeWidget', widget); | ||||
| 		}; | ||||
| 		const updateWidget = (id, data) => { | ||||
| 			context.emit('updateWidget', { id, data }); | ||||
| 		}; | ||||
| 		const widgets_ = computed({ | ||||
| 			get: () => props.widgets, | ||||
| 			set: (value) => { | ||||
| 				context.emit('updateWidgets', value); | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
| 		removeWidget(widget) { | ||||
| 			this.$emit('removeWidget', widget); | ||||
| 		}, | ||||
| 
 | ||||
| 		updateWidget(id, data) { | ||||
| 			this.$emit('updateWidget', { id, data }); | ||||
| 		}, | ||||
| 	} | ||||
| 		return { | ||||
| 			widgetRefs, | ||||
| 			configWidget, | ||||
| 			widgetAdderSelected, | ||||
| 			widgetDefs, | ||||
| 			addWidget, | ||||
| 			removeWidget, | ||||
| 			updateWidget, | ||||
| 			widgets_, | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,11 +21,39 @@ export type FormItem = { | |||
| 	default: string | null; | ||||
| 	hidden?: boolean; | ||||
| 	enum: string[]; | ||||
| } | { | ||||
| 	label?: string; | ||||
| 	type: 'radio'; | ||||
| 	default: unknown | null; | ||||
| 	hidden?: boolean; | ||||
| 	options: { | ||||
| 		label: string; | ||||
| 		value: unknown; | ||||
| 	}[]; | ||||
| } | { | ||||
| 	label?: string; | ||||
| 	type: 'object'; | ||||
| 	default: Record<string, unknown> | null; | ||||
| 	hidden: true; | ||||
| } | { | ||||
| 	label?: string; | ||||
| 	type: 'array'; | ||||
| 	default: unknown[] | null; | ||||
| 	hidden?: boolean; | ||||
| 	hidden: true; | ||||
| }; | ||||
| 
 | ||||
| export type Form = Record<string, FormItem>; | ||||
| 
 | ||||
| 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 'enum' ? string : | ||||
| 	Item['type'] extends 'array' ? unknown[] : | ||||
| 	Item['type'] extends 'object' ? Record<string, unknown> | ||||
| 	: never; | ||||
| 
 | ||||
| export type GetFormResultType<F extends Form> = { | ||||
| 	[P in keyof F]: GetItemType<F[P]>; | ||||
| }; | ||||
|  |  | |||
|  | @ -1,82 +1,89 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader" :naked="props.transparent"> | ||||
| <MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent"> | ||||
| 	<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template> | ||||
| 	<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
| 		<template v-else> | ||||
| 			<XCalendar v-show="props.view === 0" :data="[].concat(activity)"/> | ||||
| 			<XChart v-show="props.view === 1" :data="[].concat(activity)"/> | ||||
| 			<XCalendar v-show="widgetProps.view === 0" :data="[].concat(activity)"/> | ||||
| 			<XChart v-show="widgetProps.view === 1" :data="[].concat(activity)"/> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import define from './define'; | ||||
| import XCalendar from './activity.calendar.vue'; | ||||
| import XChart from './activity.chart.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'activity', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		view: { | ||||
| 			type: 'number', | ||||
| 			default: 0, | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'activity'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	view: { | ||||
| 		type: 'number' as const, | ||||
| 		default: 0, | ||||
| 		hidden: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure, save } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const activity = ref(null); | ||||
| const fetching = ref(true); | ||||
| 
 | ||||
| const toggleView = () => { | ||||
| 	if (widgetProps.view === 1) { | ||||
| 		widgetProps.view = 0; | ||||
| 	} else { | ||||
| 		widgetProps.view++; | ||||
| 	} | ||||
| 	save(); | ||||
| }; | ||||
| 
 | ||||
| os.api('charts/user/notes', { | ||||
| 	userId: $i.id, | ||||
| 	span: 'day', | ||||
| 	limit: 7 * 21, | ||||
| }).then(res => { | ||||
| 	activity.value = res.diffs.normal.map((_, i) => ({ | ||||
| 		total: res.diffs.normal[i] + res.diffs.reply[i] + res.diffs.renote[i], | ||||
| 		notes: res.diffs.normal[i], | ||||
| 		replies: res.diffs.reply[i], | ||||
| 		renotes: res.diffs.renote[i] | ||||
| 	})); | ||||
| 	fetching.value = false; | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XCalendar, | ||||
| 		XChart, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			activity: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		os.api('charts/user/notes', { | ||||
| 			userId: this.$i.id, | ||||
| 			span: 'day', | ||||
| 			limit: 7 * 21 | ||||
| 		}).then(activity => { | ||||
| 			this.activity = activity.diffs.normal.map((_, i) => ({ | ||||
| 				total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i], | ||||
| 				notes: activity.diffs.normal[i], | ||||
| 				replies: activity.diffs.reply[i], | ||||
| 				renotes: activity.diffs.renote[i] | ||||
| 			})); | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggleView() { | ||||
| 			if (this.props.view === 1) { | ||||
| 				this.props.view = 0; | ||||
| 			} else { | ||||
| 				this.props.view++; | ||||
| 			} | ||||
| 			this.save(); | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,51 +1,65 @@ | |||
| <template> | ||||
| <MkContainer :naked="props.transparent" :show-header="false"> | ||||
| <MkContainer :naked="widgetProps.transparent" :show-header="false"> | ||||
| 	<iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import define from './define'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import * as os from '@/os'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, reactive, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'ai', | ||||
| 	props: () => ({ | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'ai'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const live2d = ref<HTMLIFrameElement>(); | ||||
| 
 | ||||
| const touched = () => { | ||||
| 	//if (this.live2d) this.live2d.changeExpression('gurugurume'); | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	const onMousemove = (ev: MouseEvent) => { | ||||
| 		const iframeRect = live2d.value.getBoundingClientRect(); | ||||
| 		live2d.value.contentWindow.postMessage({ | ||||
| 			type: 'moveCursor', | ||||
| 			body: { | ||||
| 				x: ev.clientX - iframeRect.left, | ||||
| 				y: ev.clientY - iframeRect.top, | ||||
| 			} | ||||
| 		}, '*'); | ||||
| 	}; | ||||
| 
 | ||||
| 	window.addEventListener('mousemove', onMousemove, { passive: true }); | ||||
| 	onUnmounted(() => { | ||||
| 		window.removeEventListener('mousemove', onMousemove); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		window.addEventListener('mousemove', ev => { | ||||
| 			const iframeRect = this.$refs.live2d.getBoundingClientRect(); | ||||
| 			this.$refs.live2d.contentWindow.postMessage({ | ||||
| 				type: 'moveCursor', | ||||
| 				body: { | ||||
| 					x: ev.clientX - iframeRect.left, | ||||
| 					y: ev.clientY - iframeRect.top, | ||||
| 				} | ||||
| 			}, '*'); | ||||
| 		}, { passive: true }); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		touched() { | ||||
| 			//if (this.live2d) this.live2d.changeExpression('gurugurume'); | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader"> | ||||
| <MkContainer :show-header="widgetProps.showHeader"> | ||||
| 	<template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template> | ||||
| 
 | ||||
| 	<div class="uylguesu _monospace"> | ||||
| 		<textarea v-model="props.script" placeholder="(1 + 1)"></textarea> | ||||
| 		<textarea v-model="widgetProps.script" placeholder="(1 + 1)"></textarea> | ||||
| 		<button class="_buttonPrimary" @click="run">RUN</button> | ||||
| 		<div class="logs"> | ||||
| 			<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div> | ||||
|  | @ -12,97 +12,109 @@ | |||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref, watch } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import { AiScript, parse, utils } from '@syuilo/aiscript'; | ||||
| import { createAiScriptEnv } from '@/scripts/aiscript/api'; | ||||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'aiscript', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		script: { | ||||
| 			type: 'string', | ||||
| 			multiline: true, | ||||
| 			default: '(1 + 1)', | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 	}) | ||||
| }); | ||||
| const name = 'aiscript'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			logs: [], | ||||
| 		}; | ||||
| 	script: { | ||||
| 		type: 'string' as const, | ||||
| 		multiline: true, | ||||
| 		default: '(1 + 1)', | ||||
| 		hidden: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async run() { | ||||
| 			this.logs = []; | ||||
| 			const aiscript = new AiScript(createAiScriptEnv({ | ||||
| 				storageKey: 'widget', | ||||
| 				token: this.$i?.token, | ||||
| 			}), { | ||||
| 				in: (q) => { | ||||
| 					return new Promise(ok => { | ||||
| 						os.inputText({ | ||||
| 							title: q, | ||||
| 						}).then(({ canceled, result: a }) => { | ||||
| 							ok(a); | ||||
| 						}); | ||||
| 					}); | ||||
| 				}, | ||||
| 				out: (value) => { | ||||
| 					this.logs.push({ | ||||
| 						id: Math.random(), | ||||
| 						text: value.type === 'str' ? value.value : utils.valToString(value), | ||||
| 						print: true | ||||
| 					}); | ||||
| 				}, | ||||
| 				log: (type, params) => { | ||||
| 					switch (type) { | ||||
| 						case 'end': this.logs.push({ | ||||
| 							id: Math.random(), | ||||
| 							text: utils.valToString(params.val, true), | ||||
| 							print: false | ||||
| 						}); break; | ||||
| 						default: break; | ||||
| 					} | ||||
| 				} | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const logs = ref<{ | ||||
| 	id: string; | ||||
| 	text: string; | ||||
| 	print: boolean; | ||||
| }[]>([]); | ||||
| 
 | ||||
| const run = async () => { | ||||
| 	logs.value = []; | ||||
| 	const aiscript = new AiScript(createAiScriptEnv({ | ||||
| 		storageKey: 'widget', | ||||
| 		token: $i?.token, | ||||
| 	}), { | ||||
| 		in: (q) => { | ||||
| 			return new Promise(ok => { | ||||
| 				os.inputText({ | ||||
| 					title: q, | ||||
| 				}).then(({ canceled, result: a }) => { | ||||
| 					ok(a); | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			let ast; | ||||
| 			try { | ||||
| 				ast = parse(this.props.script); | ||||
| 			} catch (e) { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: 'Syntax error :(' | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
| 			try { | ||||
| 				await aiscript.exec(ast); | ||||
| 			} catch (e) { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: e | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 		out: (value) => { | ||||
| 			logs.value.push({ | ||||
| 				id: Math.random().toString(), | ||||
| 				text: value.type === 'str' ? value.value : utils.valToString(value), | ||||
| 				print: true, | ||||
| 			}); | ||||
| 		}, | ||||
| 		log: (type, params) => { | ||||
| 			switch (type) { | ||||
| 				case 'end': logs.value.push({ | ||||
| 					id: Math.random().toString(), | ||||
| 					text: utils.valToString(params.val, true), | ||||
| 					print: false, | ||||
| 				}); break; | ||||
| 				default: break; | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	let ast; | ||||
| 	try { | ||||
| 		ast = parse(widgetProps.script); | ||||
| 	} catch (e) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: 'Syntax error :(', | ||||
| 		}); | ||||
| 		return; | ||||
| 	} | ||||
| 	try { | ||||
| 		await aiscript.exec(ast); | ||||
| 	} catch (e) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: e, | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,90 +1,99 @@ | |||
| <template> | ||||
| <div class="mkw-button"> | ||||
| 	<MkButton :primary="props.colored" full @click="run"> | ||||
| 		{{ props.label }} | ||||
| 	<MkButton :primary="widgetProps.colored" full @click="run"> | ||||
| 		{{ widgetProps.label }} | ||||
| 	</MkButton> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import define from './define'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref, watch } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| import { AiScript, parse, utils } from '@syuilo/aiscript'; | ||||
| import { createAiScriptEnv } from '@/scripts/aiscript/api'; | ||||
| import { $i } from '@/account'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'button', | ||||
| 	props: () => ({ | ||||
| 		label: { | ||||
| 			type: 'string', | ||||
| 			default: 'BUTTON', | ||||
| 		}, | ||||
| 		colored: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		script: { | ||||
| 			type: 'string', | ||||
| 			multiline: true, | ||||
| 			default: 'Mk:dialog("hello" "world")', | ||||
| 		}, | ||||
| 	}) | ||||
| }); | ||||
| const name = 'button'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton | ||||
| const widgetPropsDef = { | ||||
| 	label: { | ||||
| 		type: 'string' as const, | ||||
| 		default: 'BUTTON', | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 		}; | ||||
| 	colored: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		async run() { | ||||
| 			const aiscript = new AiScript(createAiScriptEnv({ | ||||
| 				storageKey: 'widget', | ||||
| 				token: this.$i?.token, | ||||
| 			}), { | ||||
| 				in: (q) => { | ||||
| 					return new Promise(ok => { | ||||
| 						os.inputText({ | ||||
| 							title: q, | ||||
| 						}).then(({ canceled, result: a }) => { | ||||
| 							ok(a); | ||||
| 						}); | ||||
| 					}); | ||||
| 				}, | ||||
| 				out: (value) => { | ||||
| 					// nop | ||||
| 				}, | ||||
| 				log: (type, params) => { | ||||
| 					// nop | ||||
| 				} | ||||
| 	script: { | ||||
| 		type: 'string' as const, | ||||
| 		multiline: true, | ||||
| 		default: 'Mk:dialog("hello" "world")', | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const run = async () => { | ||||
| 	const aiscript = new AiScript(createAiScriptEnv({ | ||||
| 		storageKey: 'widget', | ||||
| 		token: $i?.token, | ||||
| 	}), { | ||||
| 		in: (q) => { | ||||
| 			return new Promise(ok => { | ||||
| 				os.inputText({ | ||||
| 					title: q, | ||||
| 				}).then(({ canceled, result: a }) => { | ||||
| 					ok(a); | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			let ast; | ||||
| 			try { | ||||
| 				ast = parse(this.props.script); | ||||
| 			} catch (e) { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: 'Syntax error :(' | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
| 			try { | ||||
| 				await aiscript.exec(ast); | ||||
| 			} catch (e) { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: e | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 		out: (value) => { | ||||
| 			// nop | ||||
| 		}, | ||||
| 		log: (type, params) => { | ||||
| 			// nop | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	let ast; | ||||
| 	try { | ||||
| 		ast = parse(widgetProps.script); | ||||
| 	} catch (e) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: 'Syntax error :(', | ||||
| 		}); | ||||
| 		return; | ||||
| 	} | ||||
| 	try { | ||||
| 		await aiscript.exec(ast); | ||||
| 	} catch (e) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: e, | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="mkw-calendar" :class="{ _panel: !props.transparent }"> | ||||
| <div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }"> | ||||
| 	<div class="calendar" :class="{ isHoliday }"> | ||||
| 		<p class="month-and-year"> | ||||
| 			<span class="year">{{ $t('yearX', { year }) }}</span> | ||||
|  | @ -32,77 +32,87 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { onUnmounted, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'calendar', | ||||
| 	props: () => ({ | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'calendar'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const year = ref(0); | ||||
| const month = ref(0); | ||||
| const day = ref(0); | ||||
| const weekDay = ref(''); | ||||
| const yearP = ref(0); | ||||
| const monthP = ref(0); | ||||
| const dayP = ref(0); | ||||
| const isHoliday = ref(false); | ||||
| const tick = () => { | ||||
| 	const now = new Date(); | ||||
| 	const nd = now.getDate(); | ||||
| 	const nm = now.getMonth(); | ||||
| 	const ny = now.getFullYear(); | ||||
| 
 | ||||
| 	year.value = ny; | ||||
| 	month.value = nm + 1; | ||||
| 	day.value = nd; | ||||
| 	weekDay.value = [ | ||||
| 		i18n.locale._weekday.sunday, | ||||
| 		i18n.locale._weekday.monday, | ||||
| 		i18n.locale._weekday.tuesday, | ||||
| 		i18n.locale._weekday.wednesday, | ||||
| 		i18n.locale._weekday.thursday, | ||||
| 		i18n.locale._weekday.friday, | ||||
| 		i18n.locale._weekday.saturday | ||||
| 	][now.getDay()]; | ||||
| 
 | ||||
| 	const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime(); | ||||
| 	const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/; | ||||
| 	const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime(); | ||||
| 	const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime(); | ||||
| 	const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime(); | ||||
| 	const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime(); | ||||
| 
 | ||||
| 	dayP.value   = dayNumer   / dayDenom   * 100; | ||||
| 	monthP.value = monthNumer / monthDenom * 100; | ||||
| 	yearP.value  = yearNumer  / yearDenom  * 100; | ||||
| 
 | ||||
| 	isHoliday.value = now.getDay() === 0 || now.getDay() === 6; | ||||
| }; | ||||
| 
 | ||||
| tick(); | ||||
| 
 | ||||
| const intervalId = setInterval(tick, 1000); | ||||
| onUnmounted(() => { | ||||
| 	clearInterval(intervalId); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			now: new Date(), | ||||
| 			year: null, | ||||
| 			month: null, | ||||
| 			day: null, | ||||
| 			weekDay: null, | ||||
| 			yearP: null, | ||||
| 			dayP: null, | ||||
| 			monthP: null, | ||||
| 			isHoliday: null, | ||||
| 			clock: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.tick(); | ||||
| 		this.clock = setInterval(this.tick, 1000); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		tick() { | ||||
| 			const now = new Date(); | ||||
| 			const nd = now.getDate(); | ||||
| 			const nm = now.getMonth(); | ||||
| 			const ny = now.getFullYear(); | ||||
| 
 | ||||
| 			this.year = ny; | ||||
| 			this.month = nm + 1; | ||||
| 			this.day = nd; | ||||
| 			this.weekDay = [ | ||||
| 				this.$ts._weekday.sunday, | ||||
| 				this.$ts._weekday.monday, | ||||
| 				this.$ts._weekday.tuesday, | ||||
| 				this.$ts._weekday.wednesday, | ||||
| 				this.$ts._weekday.thursday, | ||||
| 				this.$ts._weekday.friday, | ||||
| 				this.$ts._weekday.saturday | ||||
| 			][now.getDay()]; | ||||
| 
 | ||||
| 			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime(); | ||||
| 			const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/; | ||||
| 			const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime(); | ||||
| 			const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime(); | ||||
| 			const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime(); | ||||
| 			const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime(); | ||||
| 
 | ||||
| 			this.dayP   = dayNumer   / dayDenom   * 100; | ||||
| 			this.monthP = monthNumer / monthDenom * 100; | ||||
| 			this.yearP  = yearNumer  / yearDenom  * 100; | ||||
| 
 | ||||
| 			this.isHoliday = now.getDay() === 0 || now.getDay() === 6; | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,45 +1,56 @@ | |||
| <template> | ||||
| <MkContainer :naked="props.transparent" :show-header="false"> | ||||
| <MkContainer :naked="widgetProps.transparent" :show-header="false"> | ||||
| 	<div class="vubelbmv"> | ||||
| 		<MkAnalogClock class="clock" :thickness="props.thickness"/> | ||||
| 		<MkAnalogClock class="clock" :thickness="widgetProps.thickness"/> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import MkAnalogClock from '@/components/analog-clock.vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'clock', | ||||
| 	props: () => ({ | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		thickness: { | ||||
| 			type: 'radio', | ||||
| 			default: 0.1, | ||||
| 			options: [{ | ||||
| 				value: 0.1, label: 'thin' | ||||
| 			}, { | ||||
| 				value: 0.2, label: 'medium' | ||||
| 			}, { | ||||
| 				value: 0.3, label: 'thick' | ||||
| 			}] | ||||
| 		} | ||||
| 	}) | ||||
| }); | ||||
| const name = 'clock'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		MkAnalogClock | ||||
| const widgetPropsDef = { | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	thickness: { | ||||
| 		type: 'radio' as const, | ||||
| 		default: 0.1, | ||||
| 		options: [{ | ||||
| 			value: 0.1, label: 'thin' | ||||
| 		}, { | ||||
| 			value: 0.2, label: 'medium' | ||||
| 		}, { | ||||
| 			value: 0.3, label: 'thick' | ||||
| 		}], | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,75 +0,0 @@ | |||
| import { defineComponent } from 'vue'; | ||||
| import { throttle } from 'throttle-debounce'; | ||||
| import { Form } from '@/scripts/form'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default function <T extends Form>(data: { | ||||
| 	name: string; | ||||
| 	props?: () => T; | ||||
| }) { | ||||
| 	return defineComponent({ | ||||
| 		props: { | ||||
| 			widget: { | ||||
| 				type: Object, | ||||
| 				required: false | ||||
| 			}, | ||||
| 			settingCallback: { | ||||
| 				required: false | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		emits: ['updateProps'], | ||||
| 
 | ||||
| 		data() { | ||||
| 			return { | ||||
| 				props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {}, | ||||
| 				save: throttle(3000, () => { | ||||
| 					this.$emit('updateProps', this.props); | ||||
| 				}), | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		computed: { | ||||
| 			id(): string { | ||||
| 				return this.widget ? this.widget.id : null; | ||||
| 			}, | ||||
| 		}, | ||||
| 
 | ||||
| 		created() { | ||||
| 			this.mergeProps(); | ||||
| 
 | ||||
| 			this.$watch('props', () => { | ||||
| 				this.mergeProps(); | ||||
| 			}, { deep: true }); | ||||
| 
 | ||||
| 			if (this.settingCallback) this.settingCallback(this.setting); | ||||
| 		}, | ||||
| 
 | ||||
| 		methods: { | ||||
| 			mergeProps() { | ||||
| 				if (data.props) { | ||||
| 					const defaultProps = data.props(); | ||||
| 					for (const prop of Object.keys(defaultProps)) { | ||||
| 						if (this.props.hasOwnProperty(prop)) continue; | ||||
| 						this.props[prop] = defaultProps[prop].default; | ||||
| 					} | ||||
| 				} | ||||
| 			}, | ||||
| 
 | ||||
| 			async setting() { | ||||
| 				const form = data.props(); | ||||
| 				for (const item of Object.keys(form)) { | ||||
| 					form[item].default = this.props[item]; | ||||
| 				} | ||||
| 				const { canceled, result } = await os.form(data.name, form); | ||||
| 				if (canceled) return; | ||||
| 
 | ||||
| 				for (const key of Object.keys(result)) { | ||||
| 					this.props[key] = result[key]; | ||||
| 				} | ||||
| 
 | ||||
| 				this.save(); | ||||
| 			}, | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
|  | @ -1,73 +1,84 @@ | |||
| <template> | ||||
| <div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }"> | ||||
| <div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }"> | ||||
| 	<span> | ||||
| 		<span v-text="hh"></span> | ||||
| 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> | ||||
| 		<span v-text="mm"></span> | ||||
| 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> | ||||
| 		<span v-text="ss"></span> | ||||
| 		<span v-if="props.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> | ||||
| 		<span v-if="props.showMs" v-text="ms"></span> | ||||
| 		<span v-if="widgetProps.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> | ||||
| 		<span v-if="widgetProps.showMs" v-text="ms"></span> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import define from './define'; | ||||
| import * as os from '@/os'; | ||||
| <script lang="ts" setup> | ||||
| import { onUnmounted, ref, watch } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'digitalClock', | ||||
| 	props: () => ({ | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		fontSize: { | ||||
| 			type: 'number', | ||||
| 			default: 1.5, | ||||
| 			step: 0.1, | ||||
| 		}, | ||||
| 		showMs: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'digitalClock'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	fontSize: { | ||||
| 		type: 'number' as const, | ||||
| 		default: 1.5, | ||||
| 		step: 0.1, | ||||
| 	}, | ||||
| 	showMs: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| let intervalId; | ||||
| const hh = ref(''); | ||||
| const mm = ref(''); | ||||
| const ss = ref(''); | ||||
| const ms = ref(''); | ||||
| const showColon = ref(true); | ||||
| const tick = () => { | ||||
| 	const now = new Date(); | ||||
| 	hh.value = now.getHours().toString().padStart(2, '0'); | ||||
| 	mm.value = now.getMinutes().toString().padStart(2, '0'); | ||||
| 	ss.value = now.getSeconds().toString().padStart(2, '0'); | ||||
| 	ms.value = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0'); | ||||
| 	showColon.value = now.getSeconds() % 2 === 0; | ||||
| }; | ||||
| 
 | ||||
| tick(); | ||||
| 
 | ||||
| watch(() => widgetProps.showMs, () => { | ||||
| 	if (intervalId) clearInterval(intervalId); | ||||
| 	intervalId = setInterval(tick, widgetProps.showMs ? 10 : 1000); | ||||
| }, { immediate: true }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
| 	clearInterval(intervalId); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			clock: null, | ||||
| 			hh: null, | ||||
| 			mm: null, | ||||
| 			ss: null, | ||||
| 			ms: null, | ||||
| 			showColon: true, | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.tick(); | ||||
| 		this.$watch(() => this.props.showMs, () => { | ||||
| 			if (this.clock) clearInterval(this.clock); | ||||
| 			this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000); | ||||
| 		}, { immediate: true }); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		tick() { | ||||
| 			const now = new Date(); | ||||
| 			this.hh = now.getHours().toString().padStart(2, '0'); | ||||
| 			this.mm = now.getMinutes().toString().padStart(2, '0'); | ||||
| 			this.ss = now.getSeconds().toString().padStart(2, '0'); | ||||
| 			this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0'); | ||||
| 			this.showColon = now.getSeconds() % 2 === 0; | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable"> | ||||
| <MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable"> | ||||
| 	<template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template> | ||||
| 
 | ||||
| 	<div class="wbrkwalb"> | ||||
|  | @ -18,66 +18,64 @@ | |||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import define from './define'; | ||||
| import MkMiniChart from '@/components/mini-chart.vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'federation', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'federation'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps> & { foldable?: boolean; scrollable?: boolean; }>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; foldable?: boolean; scrollable?: boolean; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const instances = ref([]); | ||||
| const charts = ref([]); | ||||
| const fetching = ref(true); | ||||
| 
 | ||||
| const fetch = async () => { | ||||
| 	const instances = await os.api('federation/instances', { | ||||
| 		sort: '+lastCommunicatedAt', | ||||
| 		limit: 5 | ||||
| 	}); | ||||
| 	const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); | ||||
| 	instances.value = instances; | ||||
| 	charts.value = charts; | ||||
| 	fetching.value = false; | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	fetch(); | ||||
| 	const intervalId = setInterval(fetch, 1000 * 60); | ||||
| 	onUnmounted(() => { | ||||
| 		clearInterval(intervalId); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, MkMiniChart | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	props: { | ||||
| 		foldable: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		scrollable: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			instances: [], | ||||
| 			charts: [], | ||||
| 			fetching: true, | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 		this.clock = setInterval(this.fetch, 1000 * 60); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		async fetch() { | ||||
| 			const instances = await os.api('federation/instances', { | ||||
| 				sort: '+lastCommunicatedAt', | ||||
| 				limit: 5 | ||||
| 			}); | ||||
| 			const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); | ||||
| 			this.instances = instances; | ||||
| 			this.charts = charts; | ||||
| 			this.fetching = false; | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,134 +1,146 @@ | |||
| <template> | ||||
| <div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }"> | ||||
| <div class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }"> | ||||
| 	<div class="inbox"> | ||||
| 		<div class="label">Inbox queue<i v-if="inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div> | ||||
| 		<div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div> | ||||
| 		<div class="values"> | ||||
| 			<div> | ||||
| 				<div>Process</div> | ||||
| 				<div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div> | ||||
| 				<div :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(current.inbox.activeSincePrevTick) }}</div> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<div>Active</div> | ||||
| 				<div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div> | ||||
| 				<div :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }">{{ number(current.inbox.active) }}</div> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<div>Delayed</div> | ||||
| 				<div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div> | ||||
| 				<div :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }">{{ number(current.inbox.delayed) }}</div> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<div>Waiting</div> | ||||
| 				<div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div> | ||||
| 				<div :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }">{{ number(current.inbox.waiting) }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="deliver"> | ||||
| 		<div class="label">Deliver queue<i v-if="deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div> | ||||
| 		<div class="label">Deliver queue<i v-if="current.deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div> | ||||
| 		<div class="values"> | ||||
| 			<div> | ||||
| 				<div>Process</div> | ||||
| 				<div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div> | ||||
| 				<div :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(current.deliver.activeSincePrevTick) }}</div> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<div>Active</div> | ||||
| 				<div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div> | ||||
| 				<div :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }">{{ number(current.deliver.active) }}</div> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<div>Delayed</div> | ||||
| 				<div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div> | ||||
| 				<div :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }">{{ number(current.deliver.delayed) }}</div> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<div>Waiting</div> | ||||
| 				<div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div> | ||||
| 				<div :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }">{{ number(current.deliver.waiting) }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import define from './define'; | ||||
| import * as os from '@/os'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, reactive, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import { stream } from '@/stream'; | ||||
| import number from '@/filters/number'; | ||||
| import * as sound from '@/scripts/sound'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'jobQueue', | ||||
| 	props: () => ({ | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		sound: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'jobQueue'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	sound: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const connection = stream.useChannel('queueStats'); | ||||
| const current = reactive({ | ||||
| 	inbox: { | ||||
| 		activeSincePrevTick: 0, | ||||
| 		active: 0, | ||||
| 		waiting: 0, | ||||
| 		delayed: 0, | ||||
| 	}, | ||||
| 	deliver: { | ||||
| 		activeSincePrevTick: 0, | ||||
| 		active: 0, | ||||
| 		waiting: 0, | ||||
| 		delayed: 0, | ||||
| 	}, | ||||
| }); | ||||
| const prev = reactive({} as typeof current); | ||||
| const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1); | ||||
| 
 | ||||
| for (const domain of ['inbox', 'deliver']) { | ||||
| 	prev[domain] = JSON.parse(JSON.stringify(current[domain])); | ||||
| } | ||||
| 
 | ||||
| const onStats = (stats) => { | ||||
| 	for (const domain of ['inbox', 'deliver']) { | ||||
| 		prev[domain] = JSON.parse(JSON.stringify(current[domain])); | ||||
| 		current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick; | ||||
| 		current[domain].active = stats[domain].active; | ||||
| 		current[domain].waiting = stats[domain].waiting; | ||||
| 		current[domain].delayed = stats[domain].delayed; | ||||
| 
 | ||||
| 		if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) { | ||||
| 			jammedSound.play(); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const onStatsLog = (statsLog) => { | ||||
| 	for (const stats of [...statsLog].reverse()) { | ||||
| 		onStats(stats); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| connection.on('stats', onStats); | ||||
| connection.on('statsLog', onStatsLog); | ||||
| 
 | ||||
| connection.send('requestLog', { | ||||
| 	id: Math.random().toString().substr(2, 8), | ||||
| 	length: 1, | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			connection: markRaw(stream.useChannel('queueStats')), | ||||
| 			inbox: { | ||||
| 				activeSincePrevTick: 0, | ||||
| 				active: 0, | ||||
| 				waiting: 0, | ||||
| 				delayed: 0, | ||||
| 			}, | ||||
| 			deliver: { | ||||
| 				activeSincePrevTick: 0, | ||||
| 				active: 0, | ||||
| 				waiting: 0, | ||||
| 				delayed: 0, | ||||
| 			}, | ||||
| 			prev: {}, | ||||
| 			sound: sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1) | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		for (const domain of ['inbox', 'deliver']) { | ||||
| 			this.prev[domain] = JSON.parse(JSON.stringify(this[domain])); | ||||
| 		} | ||||
| 	 | ||||
| 		this.connection.on('stats', this.onStats); | ||||
| 		this.connection.on('statsLog', this.onStatsLog); | ||||
| onUnmounted(() => { | ||||
| 	connection.off('stats', onStats); | ||||
| 	connection.off('statsLog', onStatsLog); | ||||
| 	connection.dispose(); | ||||
| }); | ||||
| 
 | ||||
| 		this.connection.send('requestLog', { | ||||
| 			id: Math.random().toString().substr(2, 8), | ||||
| 			length: 1 | ||||
| 		}); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		this.connection.off('stats', this.onStats); | ||||
| 		this.connection.off('statsLog', this.onStatsLog); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onStats(stats) { | ||||
| 			for (const domain of ['inbox', 'deliver']) { | ||||
| 				this.prev[domain] = JSON.parse(JSON.stringify(this[domain])); | ||||
| 				this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick; | ||||
| 				this[domain].active = stats[domain].active; | ||||
| 				this[domain].waiting = stats[domain].waiting; | ||||
| 				this[domain].delayed = stats[domain].delayed; | ||||
| 
 | ||||
| 				if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) { | ||||
| 					this.sound.play(); | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onStatsLog(statsLog) { | ||||
| 			for (const stats of [...statsLog].reverse()) { | ||||
| 				this.onStats(stats); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		number | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader"> | ||||
| <MkContainer :show-header="widgetProps.showHeader"> | ||||
| 	<template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template> | ||||
| 
 | ||||
| 	<div class="otgbylcu"> | ||||
|  | @ -9,56 +9,60 @@ | |||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'memo', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'memo'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const text = ref<string | null>(defaultStore.state.memo); | ||||
| const changed = ref(false); | ||||
| let timeoutId; | ||||
| 
 | ||||
| const saveMemo = () => { | ||||
| 	defaultStore.set('memo', text.value); | ||||
| 	changed.value = false; | ||||
| }; | ||||
| 
 | ||||
| const onChange = () => { | ||||
| 	changed.value = true; | ||||
| 	clearTimeout(timeoutId); | ||||
| 	timeoutId = setTimeout(saveMemo, 1000); | ||||
| }; | ||||
| 
 | ||||
| watch(() => defaultStore.reactiveState.memo, newText => { | ||||
| 	text.value = newText.value; | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			text: null, | ||||
| 			changed: false, | ||||
| 			timeoutId: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.text = this.$store.state.memo; | ||||
| 
 | ||||
| 		this.$watch(() => this.$store.reactiveState.memo, text => { | ||||
| 			this.text = text; | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		onChange() { | ||||
| 			this.changed = true; | ||||
| 			clearTimeout(this.timeoutId); | ||||
| 			this.timeoutId = setTimeout(this.saveMemo, 1000); | ||||
| 		}, | ||||
| 
 | ||||
| 		saveMemo() { | ||||
| 			this.$store.set('memo', this.text); | ||||
| 			this.changed = false; | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,65 +1,68 @@ | |||
| <template> | ||||
| <MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true"> | ||||
| <MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true"> | ||||
| 	<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template> | ||||
| 	<template #func><button class="_button" @click="configure()"><i class="fas fa-cog"></i></button></template> | ||||
| 	<template #func><button class="_button" @click="configureNotification()"><i class="fas fa-cog"></i></button></template> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<XNotifications :include-types="props.includingTypes"/> | ||||
| 		<XNotifications :include-types="widgetProps.includingTypes"/> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import XNotifications from '@/components/notifications.vue'; | ||||
| import define from './define'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'notifications', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		height: { | ||||
| 			type: 'number', | ||||
| 			default: 300, | ||||
| 		}, | ||||
| 		includingTypes: { | ||||
| 			type: 'array', | ||||
| 			hidden: true, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 	}) | ||||
| }); | ||||
| const name = 'notifications'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 
 | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XNotifications, | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 		}; | ||||
| 	height: { | ||||
| 		type: 'number' as const, | ||||
| 		default: 300, | ||||
| 	}, | ||||
| 	includingTypes: { | ||||
| 		type: 'array' as const, | ||||
| 		hidden: true, | ||||
| 		default: null, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| 	methods: { | ||||
| 		configure() { | ||||
| 			os.popup(import('@/components/notification-setting-window.vue'), { | ||||
| 				includingTypes: this.props.includingTypes, | ||||
| 			}, { | ||||
| 				done: async (res) => { | ||||
| 					const { includingTypes } = res; | ||||
| 					this.props.includingTypes = includingTypes; | ||||
| 					this.save(); | ||||
| 				} | ||||
| 			}, 'closed'); | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure, save } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const configureNotification = () => { | ||||
| 	os.popup(import('@/components/notification-setting-window.vue'), { | ||||
| 		includingTypes: widgetProps.includingTypes, | ||||
| 	}, { | ||||
| 		done: async (res) => { | ||||
| 			const { includingTypes } = res; | ||||
| 			widgetProps.includingTypes = includingTypes; | ||||
| 			save(); | ||||
| 		} | ||||
| 	} | ||||
| 	}, 'closed'); | ||||
| }; | ||||
| 
 | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,48 +1,60 @@ | |||
| <template> | ||||
| <div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }"> | ||||
| <div class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }"> | ||||
| 	<I18n v-if="onlineUsersCount" :src="$ts.onlineUsersCount" text-tag="span" class="text"> | ||||
| 		<template #n><b>{{ onlineUsersCount }}</b></template> | ||||
| 	</I18n> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'onlineUsers', | ||||
| 	props: () => ({ | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'onlineUsers'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const onlineUsersCount = ref(0); | ||||
| 
 | ||||
| const tick = () => { | ||||
| 	os.api('get-online-users-count').then(res => { | ||||
| 		onlineUsersCount.value = res.count; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	tick(); | ||||
| 	const intervalId = setInterval(tick, 1000 * 15); | ||||
| 	onUnmounted(() => { | ||||
| 		clearInterval(intervalId); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			onlineUsersCount: null, | ||||
| 			clock: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.tick(); | ||||
| 		this.clock = setInterval(this.tick, 1000 * 15); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		tick() { | ||||
| 			os.api('get-online-users-count').then(res => { | ||||
| 				this.onlineUsersCount = res.count; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null"> | ||||
| <MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null"> | ||||
| 	<template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template> | ||||
| 
 | ||||
| 	<div class=""> | ||||
|  | @ -14,70 +14,77 @@ | |||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, reactive, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import { stream } from '@/stream'; | ||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'photos', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'photos'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const connection = stream.useChannel('main'); | ||||
| const images = ref([]); | ||||
| const fetching = ref(true); | ||||
| 
 | ||||
| const onDriveFileCreated = (file) => { | ||||
| 	if (/^image\/.+$/.test(file.type)) { | ||||
| 		images.value.unshift(file); | ||||
| 		if (images.value.length > 9) images.value.pop(); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const thumbnail = (image: any): string => { | ||||
| 	return defaultStore.state.disableShowingAnimatedImages | ||||
| 		? getStaticImageUrl(image.thumbnailUrl) | ||||
| 		: image.thumbnailUrl; | ||||
| }; | ||||
| 
 | ||||
| os.api('drive/stream', { | ||||
| 	type: 'image/*', | ||||
| 	limit: 9 | ||||
| }).then(res => { | ||||
| 	images.value = res; | ||||
| 	fetching.value = false; | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			images: [], | ||||
| 			fetching: true, | ||||
| 			connection: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = markRaw(stream.useChannel('main')); | ||||
| connection.on('driveFileCreated', onDriveFileCreated); | ||||
| onUnmounted(() => { | ||||
| 	connection.dispose(); | ||||
| }); | ||||
| 
 | ||||
| 		this.connection.on('driveFileCreated', this.onDriveFileCreated); | ||||
| 
 | ||||
| 		os.api('drive/stream', { | ||||
| 			type: 'image/*', | ||||
| 			limit: 9 | ||||
| 		}).then(images => { | ||||
| 			this.images = images; | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onDriveFileCreated(file) { | ||||
| 			if (/^image\/.+$/.test(file.type)) { | ||||
| 				this.images.unshift(file); | ||||
| 				if (this.images.length > 9) this.images.pop(); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		thumbnail(image: any): string { | ||||
| 			return this.$store.state.disableShowingAnimatedImages | ||||
| 				? getStaticImageUrl(image.thumbnailUrl) | ||||
| 				: image.thumbnailUrl; | ||||
| 		}, | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,22 +2,34 @@ | |||
| <XPostForm class="_panel" :fixed="true" :autofocus="false"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import XPostForm from '@/components/post-form.vue'; | ||||
| import define from './define'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'postForm', | ||||
| 	props: () => ({ | ||||
| 	}) | ||||
| }); | ||||
| const name = 'postForm'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| const widgetPropsDef = { | ||||
| }; | ||||
| 
 | ||||
| 	components: { | ||||
| 		XPostForm, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader"> | ||||
| <MkContainer :show-header="widgetProps.showHeader"> | ||||
| 	<template #header><i class="fas fa-rss-square"></i>RSS</template> | ||||
| 	<template #func><button class="_button" @click="setting"><i class="fas fa-cog"></i></button></template> | ||||
| 	<template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template> | ||||
| 
 | ||||
| 	<div class="ekmkgxbj"> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
|  | @ -12,57 +12,66 @@ | |||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref, watch } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'rss', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		url: { | ||||
| 			type: 'string', | ||||
| 			default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'rss'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	url: { | ||||
| 		type: 'string' as const, | ||||
| 		default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const items = ref([]); | ||||
| const fetching = ref(true); | ||||
| 
 | ||||
| const tick = () => { | ||||
| 	fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {}).then(res => { | ||||
| 		res.json().then(feed => { | ||||
| 			items.value = feed.items; | ||||
| 			fetching.value = false; | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| watch(() => widgetProps.url, tick); | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	tick(); | ||||
| 	const intervalId = setInterval(tick, 60000); | ||||
| 	onUnmounted(() => { | ||||
| 		clearInterval(intervalId); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			items: [], | ||||
| 			fetching: true, | ||||
| 			clock: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 		this.clock = setInterval(this.fetch, 60000); | ||||
| 		this.$watch(() => this.props.url, this.fetch); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, { | ||||
| 			}).then(res => { | ||||
| 				res.json().then(feed => { | ||||
| 					this.items = feed.items; | ||||
| 					this.fetching = false; | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,21 +1,22 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader" :naked="props.transparent"> | ||||
| <MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent"> | ||||
| 	<template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template> | ||||
| 	<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template> | ||||
| 
 | ||||
| 	<div v-if="meta" class="mkw-serverMetric"> | ||||
| 		<XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/> | ||||
| 		<XNet v-if="props.view === 1" :connection="connection" :meta="meta"/> | ||||
| 		<XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/> | ||||
| 		<XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/> | ||||
| 		<XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/> | ||||
| 		<XCpuMemory v-if="widgetProps.view === 0" :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"/> | ||||
| 		<XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/> | ||||
| 		<XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import define from '../define'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import XCpuMemory from './cpu-mem.vue'; | ||||
| import XNet from './net.vue'; | ||||
|  | @ -25,59 +26,61 @@ import XDisk from './disk.vue'; | |||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'serverMetric', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		view: { | ||||
| 			type: 'number', | ||||
| 			default: 0, | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'serverMetric'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	transparent: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	view: { | ||||
| 		type: 'number' as const, | ||||
| 		default: 0, | ||||
| 		hidden: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure, save } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const meta = ref(null); | ||||
| 
 | ||||
| os.api('server-info', {}).then(res => { | ||||
| 	meta.value = res; | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XCpuMemory, | ||||
| 		XNet, | ||||
| 		XCpu, | ||||
| 		XMemory, | ||||
| 		XDisk, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			meta: null, | ||||
| 			connection: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		os.api('server-info', {}).then(res => { | ||||
| 			this.meta = res; | ||||
| 		}); | ||||
| 		this.connection = markRaw(stream.useChannel('serverStats')); | ||||
| 	}, | ||||
| 	unmounted() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggleView() { | ||||
| 			if (this.props.view == 4) { | ||||
| 				this.props.view = 0; | ||||
| 			} else { | ||||
| 				this.props.view++; | ||||
| 			} | ||||
| 			this.save(); | ||||
| 		}, | ||||
| const toggleView = () => { | ||||
| 	if (widgetProps.view == 4) { | ||||
| 		widgetProps.view = 0; | ||||
| 	} else { | ||||
| 		widgetProps.view++; | ||||
| 	} | ||||
| 	save(); | ||||
| }; | ||||
| 
 | ||||
| const connection = stream.useChannel('serverStats'); | ||||
| onUnmounted(() => { | ||||
| 	connection.dispose(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,126 +1,116 @@ | |||
| <template> | ||||
| <div class="kvausudm _panel"> | ||||
| <div class="kvausudm _panel" :style="{ height: widgetProps.height + 'px' }"> | ||||
| 	<div @click="choose"> | ||||
| 		<p v-if="props.folderId == null"> | ||||
| 			<template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template> | ||||
| 			<template v-else>{{ $ts.folder }}</template> | ||||
| 		<p v-if="widgetProps.folderId == null"> | ||||
| 			{{ $ts.folder }} | ||||
| 		</p> | ||||
| 		<p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p> | ||||
| 		<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p> | ||||
| 		<div ref="slideA" class="slide a"></div> | ||||
| 		<div ref="slideB" class="slide b"></div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import define from './define'; | ||||
| <script lang="ts" setup> | ||||
| import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'slideshow', | ||||
| 	props: () => ({ | ||||
| 		height: { | ||||
| 			type: 'number', | ||||
| 			default: 300, | ||||
| 		}, | ||||
| 		folderId: { | ||||
| 			type: 'string', | ||||
| 			default: null, | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'slideshow'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	height: { | ||||
| 		type: 'number' as const, | ||||
| 		default: 300, | ||||
| 	}, | ||||
| 	folderId: { | ||||
| 		type: 'string' as const, | ||||
| 		default: null, | ||||
| 		hidden: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure, save } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const images = ref([]); | ||||
| const fetching = ref(true); | ||||
| const slideA = ref<HTMLElement>(); | ||||
| const slideB = ref<HTMLElement>(); | ||||
| 
 | ||||
| const change = () => { | ||||
| 	if (images.value.length == 0) return; | ||||
| 
 | ||||
| 	const index = Math.floor(Math.random() * images.value.length); | ||||
| 	const img = `url(${ images.value[index].url })`; | ||||
| 
 | ||||
| 	slideB.value.style.backgroundImage = img; | ||||
| 
 | ||||
| 	slideB.value.classList.add('anime'); | ||||
| 	setTimeout(() => { | ||||
| 		// 既にこのウィジェットがunmountされていたら要素がない | ||||
| 		if (slideA.value == null) return; | ||||
| 
 | ||||
| 		slideA.value.style.backgroundImage = img; | ||||
| 
 | ||||
| 		slideB.value.classList.remove('anime'); | ||||
| 	}, 1000); | ||||
| }; | ||||
| 
 | ||||
| const fetch = () => { | ||||
| 	fetching.value = true; | ||||
| 
 | ||||
| 	os.api('drive/files', { | ||||
| 		folderId: widgetProps.folderId, | ||||
| 		type: 'image/*', | ||||
| 		limit: 100 | ||||
| 	}).then(res => { | ||||
| 		images.value = res; | ||||
| 		fetching.value = false; | ||||
| 		slideA.value.style.backgroundImage = ''; | ||||
| 		slideB.value.style.backgroundImage = ''; | ||||
| 		change(); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const choose = () => { | ||||
| 	os.selectDriveFolder(false).then(folder => { | ||||
| 		if (folder == null) { | ||||
| 			return; | ||||
| 		} | ||||
| 		widgetProps.folderId = folder.id; | ||||
| 		save(); | ||||
| 		fetch(); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	if (widgetProps.folderId != null) { | ||||
| 		fetch(); | ||||
| 	} | ||||
| 
 | ||||
| 	const intervalId = setInterval(change, 10000); | ||||
| 	onUnmounted(() => { | ||||
| 		clearInterval(intervalId); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			images: [], | ||||
| 			fetching: true, | ||||
| 			clock: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			this.applySize(); | ||||
| 		}); | ||||
| 
 | ||||
| 		if (this.props.folderId != null) { | ||||
| 			this.fetch(); | ||||
| 		} | ||||
| 
 | ||||
| 		this.clock = setInterval(this.change, 10000); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		applySize() { | ||||
| 			let h; | ||||
| 
 | ||||
| 			if (this.props.size == 1) { | ||||
| 				h = 250; | ||||
| 			} else { | ||||
| 				h = 170; | ||||
| 			} | ||||
| 
 | ||||
| 			this.$el.style.height = `${h}px`; | ||||
| 		}, | ||||
| 		resize() { | ||||
| 			if (this.props.size == 1) { | ||||
| 				this.props.size = 0; | ||||
| 			} else { | ||||
| 				this.props.size++; | ||||
| 			} | ||||
| 			this.save(); | ||||
| 
 | ||||
| 			this.applySize(); | ||||
| 		}, | ||||
| 		change() { | ||||
| 			if (this.images.length == 0) return; | ||||
| 
 | ||||
| 			const index = Math.floor(Math.random() * this.images.length); | ||||
| 			const img = `url(${ this.images[index].url })`; | ||||
| 
 | ||||
| 			(this.$refs.slideB as any).style.backgroundImage = img; | ||||
| 
 | ||||
| 			this.$refs.slideB.classList.add('anime'); | ||||
| 			setTimeout(() => { | ||||
| 				// 既にこのウィジェットがunmountされていたら要素がない | ||||
| 				if ((this.$refs.slideA as any) == null) return; | ||||
| 
 | ||||
| 				(this.$refs.slideA as any).style.backgroundImage = img; | ||||
| 
 | ||||
| 				this.$refs.slideB.classList.remove('anime'); | ||||
| 			}, 1000); | ||||
| 		}, | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			os.api('drive/files', { | ||||
| 				folderId: this.props.folderId, | ||||
| 				type: 'image/*', | ||||
| 				limit: 100 | ||||
| 			}).then(images => { | ||||
| 				this.images = images; | ||||
| 				this.fetching = false; | ||||
| 				(this.$refs.slideA as any).style.backgroundImage = ''; | ||||
| 				(this.$refs.slideB as any).style.backgroundImage = ''; | ||||
| 				this.change(); | ||||
| 			}); | ||||
| 		}, | ||||
| 		choose() { | ||||
| 			os.selectDriveFolder(false).then(folder => { | ||||
| 				if (folder == null) { | ||||
| 					return; | ||||
| 				} | ||||
| 				this.props.folderId = folder.id; | ||||
| 				this.save(); | ||||
| 				this.fetch(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,116 +1,129 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true"> | ||||
| <MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true"> | ||||
| 	<template #header> | ||||
| 		<button class="_button" @click="choose"> | ||||
| 			<i v-if="props.src === 'home'" class="fas fa-home"></i> | ||||
| 			<i v-else-if="props.src === 'local'" class="fas fa-comments"></i> | ||||
| 			<i v-else-if="props.src === 'social'" class="fas fa-share-alt"></i> | ||||
| 			<i v-else-if="props.src === 'global'" class="fas fa-globe"></i> | ||||
| 			<i v-else-if="props.src === 'list'" class="fas fa-list-ul"></i> | ||||
| 			<i v-else-if="props.src === 'antenna'" class="fas fa-satellite"></i> | ||||
| 			<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span> | ||||
| 			<i v-if="widgetProps.src === 'home'" class="fas fa-home"></i> | ||||
| 			<i v-else-if="widgetProps.src === 'local'" class="fas fa-comments"></i> | ||||
| 			<i v-else-if="widgetProps.src === 'social'" class="fas fa-share-alt"></i> | ||||
| 			<i v-else-if="widgetProps.src === 'global'" class="fas fa-globe"></i> | ||||
| 			<i v-else-if="widgetProps.src === 'list'" class="fas fa-list-ul"></i> | ||||
| 			<i v-else-if="widgetProps.src === 'antenna'" class="fas fa-satellite"></i> | ||||
| 			<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.src) }}</span> | ||||
| 			<i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i> | ||||
| 		</button> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<XTimeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/> | ||||
| 		<XTimeline :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"/> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import * as os from '@/os'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import XTimeline from '@/components/timeline.vue'; | ||||
| import define from './define'; | ||||
| import * as os from '@/os'; | ||||
| import { $i } from '@/account'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'timeline', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		height: { | ||||
| 			type: 'number', | ||||
| 			default: 300, | ||||
| 		}, | ||||
| 		src: { | ||||
| 			type: 'string', | ||||
| 			default: 'home', | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 		list: { | ||||
| 			type: 'object', | ||||
| 			default: null, | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 	}) | ||||
| }); | ||||
| const name = 'timeline'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XTimeline, | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			menuOpened: false, | ||||
| 		}; | ||||
| 	height: { | ||||
| 		type: 'number' as const, | ||||
| 		default: 300, | ||||
| 	}, | ||||
| 	src: { | ||||
| 		type: 'string' as const, | ||||
| 		default: 'home', | ||||
| 		hidden: true, | ||||
| 	}, | ||||
| 	antenna: { | ||||
| 		type: 'object' as const, | ||||
| 		default: null, | ||||
| 		hidden: true, | ||||
| 	}, | ||||
| 	list: { | ||||
| 		type: 'object' as const, | ||||
| 		default: null, | ||||
| 		hidden: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async choose(ev) { | ||||
| 			this.menuOpened = true; | ||||
| 			const [antennas, lists] = await Promise.all([ | ||||
| 				os.api('antennas/list'), | ||||
| 				os.api('users/lists/list') | ||||
| 			]); | ||||
| 			const antennaItems = antennas.map(antenna => ({ | ||||
| 				text: antenna.name, | ||||
| 				icon: 'fas fa-satellite', | ||||
| 				action: () => { | ||||
| 					this.props.antenna = antenna; | ||||
| 					this.setSrc('antenna'); | ||||
| 				} | ||||
| 			})); | ||||
| 			const listItems = lists.map(list => ({ | ||||
| 				text: list.name, | ||||
| 				icon: 'fas fa-list-ul', | ||||
| 				action: () => { | ||||
| 					this.props.list = list; | ||||
| 					this.setSrc('list'); | ||||
| 				} | ||||
| 			})); | ||||
| 			os.popupMenu([{ | ||||
| 				text: this.$ts._timelines.home, | ||||
| 				icon: 'fas fa-home', | ||||
| 				action: () => { this.setSrc('home') } | ||||
| 			}, { | ||||
| 				text: this.$ts._timelines.local, | ||||
| 				icon: 'fas fa-comments', | ||||
| 				action: () => { this.setSrc('local') } | ||||
| 			}, { | ||||
| 				text: this.$ts._timelines.social, | ||||
| 				icon: 'fas fa-share-alt', | ||||
| 				action: () => { this.setSrc('social') } | ||||
| 			}, { | ||||
| 				text: this.$ts._timelines.global, | ||||
| 				icon: 'fas fa-globe', | ||||
| 				action: () => { this.setSrc('global') } | ||||
| 			}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => { | ||||
| 				this.menuOpened = false; | ||||
| 			}); | ||||
| 		}, | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| 		setSrc(src) { | ||||
| 			this.props.src = src; | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 	} | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure, save } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const menuOpened = ref(false); | ||||
| 
 | ||||
| const setSrc = (src) => { | ||||
| 	widgetProps.src = src; | ||||
| 	save(); | ||||
| }; | ||||
| 
 | ||||
| const choose = async (ev) => { | ||||
| 	menuOpened.value = true; | ||||
| 	const [antennas, lists] = await Promise.all([ | ||||
| 		os.api('antennas/list'), | ||||
| 		os.api('users/lists/list') | ||||
| 	]); | ||||
| 	const antennaItems = antennas.map(antenna => ({ | ||||
| 		text: antenna.name, | ||||
| 		icon: 'fas fa-satellite', | ||||
| 		action: () => { | ||||
| 			widgetProps.antenna = antenna; | ||||
| 			setSrc('antenna'); | ||||
| 		} | ||||
| 	})); | ||||
| 	const listItems = lists.map(list => ({ | ||||
| 		text: list.name, | ||||
| 		icon: 'fas fa-list-ul', | ||||
| 		action: () => { | ||||
| 			widgetProps.list = list; | ||||
| 			setSrc('list'); | ||||
| 		} | ||||
| 	})); | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.locale._timelines.home, | ||||
| 		icon: 'fas fa-home', | ||||
| 		action: () => { setSrc('home') } | ||||
| 	}, { | ||||
| 		text: i18n.locale._timelines.local, | ||||
| 		icon: 'fas fa-comments', | ||||
| 		action: () => { setSrc('local') } | ||||
| 	}, { | ||||
| 		text: i18n.locale._timelines.social, | ||||
| 		icon: 'fas fa-share-alt', | ||||
| 		action: () => { setSrc('social') } | ||||
| 	}, { | ||||
| 		text: i18n.locale._timelines.global, | ||||
| 		icon: 'fas fa-globe', | ||||
| 		action: () => { setSrc('global') } | ||||
| 	}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => { | ||||
| 		menuOpened.value = false; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader"> | ||||
| <MkContainer :show-header="widgetProps.showHeader"> | ||||
| 	<template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template> | ||||
| 
 | ||||
| 	<div class="wbrkwala"> | ||||
|  | @ -17,49 +17,59 @@ | |||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import define from './define'; | ||||
| import MkMiniChart from '@/components/mini-chart.vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const widget = define({ | ||||
| 	name: 'hashtags', | ||||
| 	props: () => ({ | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| const name = 'hashtags'; | ||||
| 
 | ||||
| const widgetPropsDef = { | ||||
| 	showHeader: { | ||||
| 		type: 'boolean' as const, | ||||
| 		default: true, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||
| 
 | ||||
| // 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない | ||||
| //const props = defineProps<WidgetComponentProps<WidgetProps>>(); | ||||
| //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); | ||||
| const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); | ||||
| const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); | ||||
| 
 | ||||
| const { widgetProps, configure } = useWidgetPropsManager(name, | ||||
| 	widgetPropsDef, | ||||
| 	props, | ||||
| 	emit, | ||||
| ); | ||||
| 
 | ||||
| const stats = ref([]); | ||||
| const fetching = ref(true); | ||||
| 
 | ||||
| const fetch = () => { | ||||
| 	os.api('hashtags/trend').then(stats => { | ||||
| 		stats.value = stats; | ||||
| 		fetching.value = false; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	fetch(); | ||||
| 	const intervalId = setInterval(fetch, 1000 * 60); | ||||
| 	onUnmounted(() => { | ||||
| 		clearInterval(intervalId); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, MkMiniChart | ||||
| 	}, | ||||
| 	extends: widget, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			stats: [], | ||||
| 			fetching: true, | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 		this.clock = setInterval(this.fetch, 1000 * 60); | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			os.api('hashtags/trend').then(stats => { | ||||
| 				this.stats = stats; | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| defineExpose<WidgetComponentExpose>({ | ||||
| 	name, | ||||
| 	configure, | ||||
| 	id: props.widget ? props.widget.id : null, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										71
									
								
								packages/client/src/widgets/widget.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								packages/client/src/widgets/widget.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| import { reactive, watch } from 'vue'; | ||||
| import { throttle } from 'throttle-debounce'; | ||||
| import { Form, GetFormResultType } from '@/scripts/form'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export type Widget<P extends Record<string, unknown>> = { | ||||
| 	id: string; | ||||
| 	data: Partial<P>; | ||||
| }; | ||||
| 
 | ||||
| export type WidgetComponentProps<P extends Record<string, unknown>> = { | ||||
| 	widget?: Widget<P>; | ||||
| }; | ||||
| 
 | ||||
| export type WidgetComponentEmits<P extends Record<string, unknown>> = { | ||||
| 	(e: 'updateProps', props: P); | ||||
| }; | ||||
| 
 | ||||
| export type WidgetComponentExpose = { | ||||
| 	name: string; | ||||
| 	id: string | null; | ||||
| 	configure: () => void; | ||||
| }; | ||||
| 
 | ||||
| export const useWidgetPropsManager = <F extends Form & Record<string, { default: any; }>>( | ||||
| 	name: string, | ||||
| 	propsDef: F, | ||||
| 	props: Readonly<WidgetComponentProps<GetFormResultType<F>>>, | ||||
| 	emit: WidgetComponentEmits<GetFormResultType<F>>, | ||||
| ): { | ||||
| 	widgetProps: GetFormResultType<F>; | ||||
| 	save: () => void; | ||||
| 	configure: () => void; | ||||
| } => { | ||||
| 	const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {}); | ||||
| 
 | ||||
| 	const mergeProps = () => { | ||||
| 		for (const prop of Object.keys(propsDef)) { | ||||
| 			if (widgetProps.hasOwnProperty(prop)) continue; | ||||
| 			widgetProps[prop] = propsDef[prop].default; | ||||
| 		} | ||||
| 	}; | ||||
| 	watch(widgetProps, () => { | ||||
| 		mergeProps(); | ||||
| 	}, { deep: true, immediate: true, }); | ||||
| 
 | ||||
| 	const save = throttle(3000, () => { | ||||
| 		emit('updateProps', widgetProps) | ||||
| 	}); | ||||
| 
 | ||||
| 	const configure = async () => { | ||||
| 		const form = JSON.parse(JSON.stringify(propsDef)); | ||||
| 		for (const item of Object.keys(form)) { | ||||
| 			form[item].default = widgetProps[item]; | ||||
| 		} | ||||
| 		const { canceled, result } = await os.form(name, form); | ||||
| 		if (canceled) return; | ||||
| 
 | ||||
| 		for (const key of Object.keys(result)) { | ||||
| 			widgetProps[key] = result[key]; | ||||
| 		} | ||||
| 
 | ||||
| 		save(); | ||||
| 	}; | ||||
| 
 | ||||
| 	return { | ||||
| 		widgetProps, | ||||
| 		save, | ||||
| 		configure, | ||||
| 	}; | ||||
| }; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue