summaryrefslogtreecommitdiff
path: root/packages/client/src/widgets
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-01-08 20:30:01 +0900
committerGitHub <noreply@github.com>2022-01-08 20:30:01 +0900
commit0bbde336b3636f4135de54c0ed75c7aa208534fe (patch)
tree45bb6a5c1e5397b3de351068d0eb1eeb6e3ec6c9 /packages/client/src/widgets
parentbye room (diff)
downloadsharkey-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')
-rw-r--r--packages/client/src/widgets/activity.vue125
-rw-r--r--packages/client/src/widgets/aichan.vue94
-rw-r--r--packages/client/src/widgets/aiscript.vue174
-rw-r--r--packages/client/src/widgets/button.vue149
-rw-r--r--packages/client/src/widgets/calendar.vue140
-rw-r--r--packages/client/src/widgets/clock.vue73
-rw-r--r--packages/client/src/widgets/define.ts75
-rw-r--r--packages/client/src/widgets/digital-clock.vue119
-rw-r--r--packages/client/src/widgets/federation.vue108
-rw-r--r--packages/client/src/widgets/job-queue.vue176
-rw-r--r--packages/client/src/widgets/memo.vue88
-rw-r--r--packages/client/src/widgets/notifications.vue95
-rw-r--r--packages/client/src/widgets/online-users.vue80
-rw-r--r--packages/client/src/widgets/photos.vue117
-rw-r--r--packages/client/src/widgets/post-form.vue38
-rw-r--r--packages/client/src/widgets/rss.vue105
-rw-r--r--packages/client/src/widgets/server-metric/index.vue119
-rw-r--r--packages/client/src/widgets/slideshow.vue184
-rw-r--r--packages/client/src/widgets/timeline.vue201
-rw-r--r--packages/client/src/widgets/trends.vue86
-rw-r--r--packages/client/src/widgets/widget.ts71
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,
+ };
+};