diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-01-08 20:30:01 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-01-08 20:30:01 +0900 |
| commit | 0bbde336b3636f4135de54c0ed75c7aa208534fe (patch) | |
| tree | 45bb6a5c1e5397b3de351068d0eb1eeb6e3ec6c9 /packages/client/src/widgets | |
| parent | bye room (diff) | |
| download | sharkey-0bbde336b3636f4135de54c0ed75c7aa208534fe.tar.gz sharkey-0bbde336b3636f4135de54c0ed75c7aa208534fe.tar.bz2 sharkey-0bbde336b3636f4135de54c0ed75c7aa208534fe.zip | |
refactor: Widgetのcomposition api移行 (#8125)
* wip
* wip
* wip
* wip
* wip
* wip
* fix
Diffstat (limited to 'packages/client/src/widgets')
21 files changed, 1280 insertions, 1137 deletions
diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue index d322f4758a..acbbb7a97a 100644 --- a/packages/client/src/widgets/activity.vue +++ b/packages/client/src/widgets/activity.vue @@ -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'; -export default defineComponent({ - components: { - MkContainer, - XCalendar, - XChart, +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, }, - extends: widget, - data() { - return { - fetching: true, - activity: null, - }; + transparent: { + type: 'boolean' as const, + default: false, }, - 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; - }); + view: { + type: 'number' as const, + default: 0, + hidden: true, }, - methods: { - toggleView() { - if (this.props.view === 1) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - } +}; + +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; +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/aichan.vue b/packages/client/src/widgets/aichan.vue index 891b7454d1..03e394b976 100644 --- a/packages/client/src/widgets/aichan.vue +++ b/packages/client/src/widgets/aichan.vue @@ -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'; -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 }); +const widgetPropsDef = { + transparent: { + type: 'boolean' as const, + default: false, }, - methods: { - touched() { - //if (this.live2d) this.live2d.changeExpression('gurugurume'); - } - } +}; + +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); + }); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/aiscript.vue b/packages/client/src/widgets/aiscript.vue index 46c5094ee9..0a5c0d614d 100644 --- a/packages/client/src/widgets/aiscript.vue +++ b/packages/client/src/widgets/aiscript.vue @@ -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>; - 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 +// 現時点では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); }); - } + }); + }, + 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> diff --git a/packages/client/src/widgets/button.vue b/packages/client/src/widgets/button.vue index e98570862e..a33afd6e7a 100644 --- a/packages/client/src/widgets/button.vue +++ b/packages/client/src/widgets/button.vue @@ -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")', + }, +}; - 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 +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); }); - } + }); }, + 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> diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue index c8b52d7afc..d16d3424b6 100644 --- a/packages/client/src/widgets/calendar.vue +++ b/packages/client/src/widgets/calendar.vue @@ -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'; -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); +const widgetPropsDef = { + transparent: { + type: 'boolean' as const, + default: false, }, - 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()]; +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - 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(); +// 現時点では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); }>(); - this.dayP = dayNumer / dayDenom * 100; - this.monthP = monthNumer / monthDenom * 100; - this.yearP = yearNumer / yearDenom * 100; +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); - this.isHoliday = now.getDay() === 0 || now.getDay() === 6; - } - } +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); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue index 6ca7ecd430..6acb10d74d 100644 --- a/packages/client/src/widgets/clock.vue +++ b/packages/client/src/widgets/clock.vue @@ -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, + }, + thickness: { + type: 'radio' as const, + default: 0.1, + options: [{ + value: 0.1, label: 'thin' + }, { + value: 0.2, label: 'medium' + }, { + value: 0.3, label: 'thick' + }], }, - 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> diff --git a/packages/client/src/widgets/define.ts b/packages/client/src/widgets/define.ts deleted file mode 100644 index 08a346d97c..0000000000 --- a/packages/client/src/widgets/define.ts +++ /dev/null @@ -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(); - }, - } - }); -} diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue index fbf632d2de..637b0368be 100644 --- a/packages/client/src/widgets/digital-clock.vue +++ b/packages/client/src/widgets/digital-clock.vue @@ -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'; -export default defineComponent({ - extends: widget, - data() { - return { - clock: null, - hh: null, - mm: null, - ss: null, - ms: null, - showColon: true, - }; +const widgetPropsDef = { + transparent: { + type: 'boolean' as const, + default: false, }, - 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 }); + fontSize: { + type: 'number' as const, + default: 1.5, + step: 0.1, }, - beforeUnmount() { - clearInterval(this.clock); + showMs: { + type: 'boolean' as const, + default: true, }, - 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; - } - } +}; + +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); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue index 736a91c52e..5d53b683b4 100644 --- a/packages/client/src/widgets/federation.vue +++ b/packages/client/src/widgets/federation.vue @@ -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'; -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); +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, }, - 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; - } - } +}; + +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); + }); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue index 1b7c71de67..4a2a3cf233 100644 --- a/packages/client/src/widgets/job-queue.vue +++ b/packages/client/src/widgets/job-queue.vue @@ -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'; -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) - }; +const widgetPropsDef = { + transparent: { + type: 'boolean' as const, + default: false, }, - 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); + sound: { + type: 'boolean' as const, + default: false, + }, +}; + +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 1 - }); +// 現時点では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, }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - this.connection.dispose(); + deliver: { + activeSincePrevTick: 0, + active: 0, + waiting: 0, + delayed: 0, }, - 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; +}); +const prev = reactive({} as typeof current); +const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1); - if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) { - this.sound.play(); - } - } - }, +for (const domain of ['inbox', 'deliver']) { + prev[domain] = JSON.parse(JSON.stringify(current[domain])); +} - onStatsLog(statsLog) { - for (const stats of [...statsLog].reverse()) { - this.onStats(stats); - } - }, +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(); + } + } +}; - number +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, +}); + +onUnmounted(() => { + connection.off('stats', onStats); + connection.off('statsLog', onStatsLog); + connection.dispose(); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue index 9b51ada220..3dfc6eb5fa 100644 --- a/packages/client/src/widgets/memo.vue +++ b/packages/client/src/widgets/memo.vue @@ -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'; -export default defineComponent({ - components: { - MkContainer +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, }, - extends: widget, +}; - data() { - return { - text: null, - changed: false, - timeoutId: null, - }; - }, +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - created() { - this.text = this.$store.state.memo; +// 現時点では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); }>(); - this.$watch(() => this.$store.reactiveState.memo, text => { - this.text = text; - }); - }, +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); - methods: { - onChange() { - this.changed = true; - clearTimeout(this.timeoutId); - this.timeoutId = setTimeout(this.saveMemo, 1000); - }, +const text = ref<string | null>(defaultStore.state.memo); +const changed = ref(false); +let timeoutId; - saveMemo() { - this.$store.set('memo', this.text); - this.changed = false; - } - } +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; +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/notifications.vue b/packages/client/src/widgets/notifications.vue index 568705b661..8cf29c9271 100644 --- a/packages/client/src/widgets/notifications.vue +++ b/packages/client/src/widgets/notifications.vue @@ -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, - }, - }) -}); - -export default defineComponent({ +const name = 'notifications'; - 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> diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue index 5b889f4816..2d47688697 100644 --- a/packages/client/src/widgets/online-users.vue +++ b/packages/client/src/widgets/online-users.vue @@ -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'; -export default defineComponent({ - extends: widget, - data() { - return { - onlineUsersCount: null, - clock: null, - }; - }, - created() { - this.tick(); - this.clock = setInterval(this.tick, 1000 * 15); +const widgetPropsDef = { + transparent: { + type: 'boolean' as const, + default: true, }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - tick() { - os.api('get-online-users-count').then(res => { - this.onlineUsersCount = res.count; - }); - } - } +}; + +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); + }); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/photos.vue b/packages/client/src/widgets/photos.vue index 7a0b54027b..8f948dc643 100644 --- a/packages/client/src/widgets/photos.vue +++ b/packages/client/src/widgets/photos.vue @@ -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'; -export default defineComponent({ - components: { - MkContainer, +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, }, - extends: widget, - data() { - return { - images: [], - fetching: true, - connection: null, - }; + transparent: { + type: 'boolean' as const, + default: false, }, - mounted() { - this.connection = markRaw(stream.useChannel('main')); +}; - this.connection.on('driveFileCreated', this.onDriveFileCreated); +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - 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(); - } - }, +// 現時点では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, +); - thumbnail(image: any): string { - return this.$store.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, +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; +}); + +connection.on('driveFileCreated', onDriveFileCreated); +onUnmounted(() => { + connection.dispose(); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/post-form.vue b/packages/client/src/widgets/post-form.vue index 6de0574cc1..51aa8fcf6b 100644 --- a/packages/client/src/widgets/post-form.vue +++ b/packages/client/src/widgets/post-form.vue @@ -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'; + +const widgetPropsDef = { +}; + +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); }>(); -export default defineComponent({ +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); - components: { - XPostForm, - }, - extends: widget, +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue index b2dc77854e..aa82054580 100644 --- a/packages/client/src/widgets/rss.vue +++ b/packages/client/src/widgets/rss.vue @@ -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'; -export default defineComponent({ - components: { - MkContainer - }, - extends: widget, - data() { - return { - items: [], - fetching: true, - clock: null, - }; +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 60000); - this.$watch(() => this.props.url, this.fetch); + url: { + type: 'string' as const, + default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', }, - 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; - }); - }); - }, - } +}; + +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); + }); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue index 107b750906..2caa73fa74 100644 --- a/packages/client/src/widgets/server-metric/index.vue +++ b/packages/client/src/widgets/server-metric/index.vue @@ -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'; -export default defineComponent({ - components: { - MkContainer, - XCpuMemory, - XNet, - XCpu, - XMemory, - XDisk, - }, - extends: widget, - data() { - return { - meta: null, - connection: null, - }; +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, }, - created() { - os.api('server-info', {}).then(res => { - this.meta = res; - }); - this.connection = markRaw(stream.useChannel('serverStats')); + transparent: { + type: 'boolean' as const, + default: false, }, - unmounted() { - this.connection.dispose(); + view: { + type: 'number' as const, + default: 0, + hidden: true, }, - methods: { - toggleView() { - if (this.props.view == 4) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - }, +}; + +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; +}); + +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> diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue index 0909bda67c..ac0c6c9e07 100644 --- a/packages/client/src/widgets/slideshow.vue +++ b/packages/client/src/widgets/slideshow.vue @@ -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'; -export default defineComponent({ - extends: widget, - data() { - return { - images: [], - fetching: true, - clock: null - }; +const widgetPropsDef = { + height: { + type: 'number' as const, + default: 300, + }, + folderId: { + type: 'string' as const, + default: null, + hidden: true, }, - mounted() { - this.$nextTick(() => { - this.applySize(); - }); +}; - if (this.props.folderId != null) { - this.fetch(); - } +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - this.clock = setInterval(this.change, 10000); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - applySize() { - let h; +// 現時点では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); }>(); - if (this.props.size == 1) { - h = 250; - } else { - h = 170; - } +const { widgetProps, configure, save } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); - this.$el.style.height = `${h}px`; - }, - resize() { - if (this.props.size == 1) { - this.props.size = 0; - } else { - this.props.size++; - } - this.save(); +const images = ref([]); +const fetching = ref(true); +const slideA = ref<HTMLElement>(); +const slideB = ref<HTMLElement>(); - this.applySize(); - }, - change() { - if (this.images.length == 0) return; +const change = () => { + if (images.value.length == 0) return; - const index = Math.floor(Math.random() * this.images.length); - const img = `url(${ this.images[index].url })`; + const index = Math.floor(Math.random() * images.value.length); + const img = `url(${ images.value[index].url })`; - (this.$refs.slideB as any).style.backgroundImage = img; + slideB.value.style.backgroundImage = img; - this.$refs.slideB.classList.add('anime'); - setTimeout(() => { - // 既にこのウィジェットがunmountされていたら要素がない - if ((this.$refs.slideA as any) == null) return; + slideB.value.classList.add('anime'); + setTimeout(() => { + // 既にこのウィジェットがunmountされていたら要素がない + if (slideA.value == null) return; - (this.$refs.slideA as any).style.backgroundImage = img; + slideA.value.style.backgroundImage = img; - this.$refs.slideB.classList.remove('anime'); - }, 1000); - }, - fetch() { - this.fetching = true; + slideB.value.classList.remove('anime'); + }, 1000); +}; - 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(); - }); +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); + }); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue index aee6a35b1d..fa700cc8ee 100644 --- a/packages/client/src/widgets/timeline.vue +++ b/packages/client/src/widgets/timeline.vue @@ -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, + }, +}; + +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 menuOpened = ref(false); + +const setSrc = (src) => { + widgetProps.src = src; + save(); +}; - 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; - }); - }, +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; + }); +}; - setSrc(src) { - this.props.src = src; - this.save(); - }, - } +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue index ffad93c02b..3905daa673 100644 --- a/packages/client/src/widgets/trends.vue +++ b/packages/client/src/widgets/trends.vue @@ -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'; -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); +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, }, - methods: { - fetch() { - os.api('hashtags/trend').then(stats => { - this.stats = stats; - this.fetching = 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 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); + }); +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, }); </script> diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts new file mode 100644 index 0000000000..81239bfb3b --- /dev/null +++ b/packages/client/src/widgets/widget.ts @@ -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, + }; +}; |