diff options
Diffstat (limited to 'packages/client/src/widgets')
| -rw-r--r-- | packages/client/src/widgets/activity.vue | 12 | ||||
| -rw-r--r-- | packages/client/src/widgets/aichan.vue | 31 | ||||
| -rw-r--r-- | packages/client/src/widgets/calendar.vue | 25 | ||||
| -rw-r--r-- | packages/client/src/widgets/federation.vue | 16 | ||||
| -rw-r--r-- | packages/client/src/widgets/index.ts | 4 | ||||
| -rw-r--r-- | packages/client/src/widgets/instance-cloud.vue | 76 | ||||
| -rw-r--r-- | packages/client/src/widgets/online-users.vue | 12 | ||||
| -rw-r--r-- | packages/client/src/widgets/rss-ticker.vue | 144 | ||||
| -rw-r--r-- | packages/client/src/widgets/rss.vue | 26 | ||||
| -rw-r--r-- | packages/client/src/widgets/slideshow.vue | 15 | ||||
| -rw-r--r-- | packages/client/src/widgets/trends.vue | 12 | ||||
| -rw-r--r-- | packages/client/src/widgets/widget.ts | 5 |
12 files changed, 298 insertions, 80 deletions
diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue index 7fb9f5894c..7252d65403 100644 --- a/packages/client/src/widgets/activity.vue +++ b/packages/client/src/widgets/activity.vue @@ -1,6 +1,6 @@ <template> <MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" class="mkw-activity"> - <template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template> + <template #header><i class="fas fa-chart-simple"></i>{{ $ts._widgets.activity }}</template> <template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template> <div> @@ -15,12 +15,12 @@ <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 XCalendar from './activity.calendar.vue'; import XChart from './activity.chart.vue'; +import { GetFormResultType } from '@/scripts/form'; +import * as os from '@/os'; +import MkContainer from '@/components/ui/container.vue'; import { $i } from '@/account'; const name = 'activity'; @@ -67,7 +67,7 @@ const toggleView = () => { save(); }; -os.api('charts/user/notes', { +os.apiGet('charts/user/notes', { userId: $i.id, span: 'day', limit: 7 * 21, @@ -76,7 +76,7 @@ os.api('charts/user/notes', { 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] + renotes: res.diffs.renote[i], })); fetching.value = false; }); diff --git a/packages/client/src/widgets/aichan.vue b/packages/client/src/widgets/aichan.vue index cdd367cc84..828490fd9c 100644 --- a/packages/client/src/widgets/aichan.vue +++ b/packages/client/src/widgets/aichan.vue @@ -6,8 +6,8 @@ <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 { GetFormResultType } from '@/scripts/form'; const name = 'ai'; @@ -38,22 +38,23 @@ 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, - } - }, '*'); - }; +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, + }, + }, '*'); +}; +onMounted(() => { window.addEventListener('mousemove', onMousemove, { passive: true }); - onUnmounted(() => { - window.removeEventListener('mousemove', onMousemove); - }); +}); + +onUnmounted(() => { + window.removeEventListener('mousemove', onMousemove); }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue index 2a2b035541..3a0dc8970c 100644 --- a/packages/client/src/widgets/calendar.vue +++ b/packages/client/src/widgets/calendar.vue @@ -34,9 +34,10 @@ <script lang="ts" setup> import { onUnmounted, ref } from 'vue'; -import { GetFormResultType } from '@/scripts/form'; import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form'; import { i18n } from '@/i18n'; +import { useInterval } from '@/scripts/use-interval'; const name = 'calendar'; @@ -85,28 +86,26 @@ const tick = () => { i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, - i18n.ts._weekday.saturday + i18n.ts._weekday.saturday, ][now.getDay()]; - const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime(); - const dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/; + const 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(); + 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; + dayP.value = dayNumer / dayDenom * 100; monthP.value = monthNumer / monthDenom * 100; - yearP.value = yearNumer / yearDenom * 100; + yearP.value = yearNumer / yearDenom * 100; isHoliday.value = now.getDay() === 0 || now.getDay() === 6; }; -tick(); - -const intervalId = window.setInterval(tick, 1000); -onUnmounted(() => { - window.clearInterval(intervalId); +useInterval(tick, 1000, { + immediate: true, + afterMounted: false, }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue index a3862077bb..ac87cdac2e 100644 --- a/packages/client/src/widgets/federation.vue +++ b/packages/client/src/widgets/federation.vue @@ -20,11 +20,12 @@ <script lang="ts" setup> import { onMounted, onUnmounted, ref } from 'vue'; -import { GetFormResultType } from '@/scripts/form'; import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/ui/container.vue'; import MkMiniChart from '@/components/mini-chart.vue'; import * as os from '@/os'; +import { useInterval } from '@/scripts/use-interval'; const name = 'federation'; @@ -56,20 +57,17 @@ const fetching = ref(true); const fetch = async () => { const fetchedInstances = await os.api('federation/instances', { sort: '+lastCommunicatedAt', - limit: 5 + limit: 5, }); - const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); + const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.apiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); instances.value = fetchedInstances; charts.value = fetchedCharts; fetching.value = false; }; -onMounted(() => { - fetch(); - const intervalId = window.setInterval(fetch, 1000 * 60); - onUnmounted(() => { - window.clearInterval(intervalId); - }); +useInterval(fetch, 1000 * 60, { + immediate: true, + afterMounted: true, }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/index.ts b/packages/client/src/widgets/index.ts index 51a82af080..baf6acd23d 100644 --- a/packages/client/src/widgets/index.ts +++ b/packages/client/src/widgets/index.ts @@ -6,6 +6,7 @@ export default function(app: App) { app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue'))); app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue'))); app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue'))); + app.component('MkwRssTicker', defineAsyncComponent(() => import('./rss-ticker.vue'))); app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue'))); app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue'))); app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue'))); @@ -17,6 +18,7 @@ export default function(app: App) { app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue'))); app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue'))); app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue'))); + app.component('MkwInstanceCloud', defineAsyncComponent(() => import('./instance-cloud.vue'))); app.component('MkwButton', defineAsyncComponent(() => import('./button.vue'))); app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue'))); app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue'))); @@ -28,12 +30,14 @@ export const widgets = [ 'timeline', 'calendar', 'rss', + 'rssTicker', 'trends', 'clock', 'activity', 'photos', 'digitalClock', 'federation', + 'instanceCloud', 'postForm', 'slideshow', 'serverMetric', diff --git a/packages/client/src/widgets/instance-cloud.vue b/packages/client/src/widgets/instance-cloud.vue new file mode 100644 index 0000000000..597ce0e824 --- /dev/null +++ b/packages/client/src/widgets/instance-cloud.vue @@ -0,0 +1,76 @@ +<template> +<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-instance-cloud"> + <div class=""> + <MkTagCloud v-if="activeInstances"> + <li v-for="instance in activeInstances" :key="instance.id"> + <a @click.prevent="onInstanceClick(instance)"> + <img style="width: 32px;" :src="instance.iconUrl"> + </a> + </li> + </MkTagCloud> + </div> +</MkContainer> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form'; +import MkContainer from '@/components/ui/container.vue'; +import MkTagCloud from '@/components/tag-cloud.vue'; +import * as os from '@/os'; +import { useInterval } from '@/scripts/use-interval'; + +const name = 'instanceCloud'; + +const widgetPropsDef = { + transparent: { + type: 'boolean' as const, + default: false, + }, +}; + +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; + +// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない +//const props = defineProps<WidgetComponentProps<WidgetProps>>(); +//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); +const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); + +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); + +let cloud = $ref<InstanceType<typeof MkTagCloud> | null>(); +let activeInstances = $shallowRef(null); + +function onInstanceClick(i) { + os.pageWindow(`/instance-info/${i.host}`); +} + +useInterval(() => { + os.api('federation/instances', { + sort: '+lastCommunicatedAt', + limit: 25, + }).then(res => { + activeInstances = res; + if (cloud) cloud.update(); + }); +}, 1000 * 60 * 3, { + immediate: true, + afterMounted: true, +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, +}); +</script> + +<style lang="scss" scoped> + +</style> diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue index eb3184fe9d..4122a82657 100644 --- a/packages/client/src/widgets/online-users.vue +++ b/packages/client/src/widgets/online-users.vue @@ -8,9 +8,10 @@ <script lang="ts" setup> import { onMounted, onUnmounted, ref } from 'vue'; -import { GetFormResultType } from '@/scripts/form'; import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; +import { useInterval } from '@/scripts/use-interval'; const name = 'onlineUsers'; @@ -43,12 +44,9 @@ const tick = () => { }); }; -onMounted(() => { - tick(); - const intervalId = window.setInterval(tick, 1000 * 15); - onUnmounted(() => { - window.clearInterval(intervalId); - }); +useInterval(tick, 1000 * 15, { + immediate: true, + afterMounted: true, }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/rss-ticker.vue b/packages/client/src/widgets/rss-ticker.vue new file mode 100644 index 0000000000..06995bc865 --- /dev/null +++ b/packages/client/src/widgets/rss-ticker.vue @@ -0,0 +1,144 @@ +<template> +<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-rss-ticker"> + <template #header><i class="fas fa-rss-square"></i>RSS</template> + <template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template> + + <div class="ekmkgxbk"> + <MkLoading v-if="fetching"/> + <div v-else class="feed"> + <transition name="change" mode="default"> + <MarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse"> + <span v-for="item in items" class="item"> + <a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span> + </span> + </MarqueeText> + </transition> + </div> + </div> +</MkContainer> +</template> + +<script lang="ts" setup> +import { onMounted, onUnmounted, ref, watch } from 'vue'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import MarqueeText from '@/components/marquee.vue'; +import { GetFormResultType } from '@/scripts/form'; +import * as os from '@/os'; +import MkContainer from '@/components/ui/container.vue'; +import { useInterval } from '@/scripts/use-interval'; + +const name = 'rssTicker'; + +const widgetPropsDef = { + url: { + type: 'string' as const, + default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', + }, + refreshIntervalSec: { + type: 'number' as const, + default: 60, + }, + duration: { + type: 'range' as const, + default: 70, + step: 1, + min: 5, + max: 200, + }, + reverse: { + type: 'boolean' as const, + default: false, + }, + showHeader: { + type: 'boolean' as const, + default: false, + }, + transparent: { + type: 'boolean' as const, + default: false, + }, +}; + +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; + +// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない +//const props = defineProps<WidgetComponentProps<WidgetProps>>(); +//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); +const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); + +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); + +const items = ref([]); +const fetching = ref(true); +let key = $ref(0); + +const tick = () => { + fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => { + res.json().then(feed => { + items.value = feed.items; + fetching.value = false; + key++; + }); + }); +}; + +watch(() => widgetProps.url, tick); + +useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), { + immediate: true, + afterMounted: true, +}); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, +}); +</script> + +<style lang="scss" scoped> +.change-enter-active, .change-leave-active { + position: absolute; + top: 0; + transition: all 1s ease; +} +.change-enter-from { + opacity: 0; + transform: translateY(-100%); +} +.change-leave-to { + opacity: 0; + transform: translateY(100%); +} + +.ekmkgxbk { + > .feed { + --height: 42px; + padding: 0; + font-size: 0.9em; + line-height: var(--height); + height: var(--height); + contain: strict; + + ::v-deep(.item) { + display: inline-flex; + align-items: center; + vertical-align: bottom; + color: var(--fg); + + > .divider { + display: inline-block; + width: 0.5px; + height: 16px; + margin: 0 1em; + background: var(--divider); + } + } + } +} +</style> diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue index fc65f11813..72f6249820 100644 --- a/packages/client/src/widgets/rss.vue +++ b/packages/client/src/widgets/rss.vue @@ -6,7 +6,7 @@ <div class="ekmkgxbj"> <MkLoading v-if="fetching"/> <div v-else class="feed"> - <a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> + <a v-for="item in items" class="item" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> </div> </div> </MkContainer> @@ -14,22 +14,23 @@ <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 { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; import MkContainer from '@/components/ui/container.vue'; +import { useInterval } from '@/scripts/use-interval'; const name = 'rss'; const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, url: { type: 'string' as const, default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', }, + showHeader: { + type: 'boolean' as const, + default: true, + }, }; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -50,7 +51,7 @@ const items = ref([]); const fetching = ref(true); const tick = () => { - fetch(`https://api.rss2json.com/v1/api.json?rss_url=${widgetProps.url}`, {}).then(res => { + fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => { res.json().then(feed => { items.value = feed.items; fetching.value = false; @@ -60,12 +61,9 @@ const tick = () => { watch(() => widgetProps.url, tick); -onMounted(() => { - tick(); - const intervalId = window.setInterval(tick, 60000); - onUnmounted(() => { - window.clearInterval(intervalId); - }); +useInterval(tick, 60000, { + immediate: true, + afterMounted: true, }); defineExpose<WidgetComponentExpose>({ @@ -81,7 +79,7 @@ defineExpose<WidgetComponentExpose>({ padding: 0; font-size: 0.9em; - > a { + > .item { display: block; padding: 8px 16px; color: var(--fg); diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue index fd78edbe40..c286312161 100644 --- a/packages/client/src/widgets/slideshow.vue +++ b/packages/client/src/widgets/slideshow.vue @@ -13,9 +13,10 @@ <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 { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; +import { useInterval } from '@/scripts/use-interval'; const name = 'slideshow'; @@ -75,7 +76,7 @@ const fetch = () => { os.api('drive/files', { folderId: widgetProps.folderId, type: 'image/*', - limit: 100 + limit: 100, }).then(res => { images.value = res; fetching.value = false; @@ -96,15 +97,15 @@ const choose = () => { }); }; +useInterval(change, 10000, { + immediate: false, + afterMounted: true, +}); + onMounted(() => { if (widgetProps.folderId != null) { fetch(); } - - const intervalId = window.setInterval(change, 10000); - onUnmounted(() => { - window.clearInterval(intervalId); - }); }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue index 9680f1c892..0f34ea6341 100644 --- a/packages/client/src/widgets/trends.vue +++ b/packages/client/src/widgets/trends.vue @@ -19,11 +19,12 @@ <script lang="ts" setup> import { onMounted, onUnmounted, ref } from 'vue'; -import { GetFormResultType } from '@/scripts/form'; import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/ui/container.vue'; import MkMiniChart from '@/components/mini-chart.vue'; import * as os from '@/os'; +import { useInterval } from '@/scripts/use-interval'; const name = 'hashtags'; @@ -58,12 +59,9 @@ const fetch = () => { }); }; -onMounted(() => { - fetch(); - const intervalId = window.setInterval(fetch, 1000 * 60); - onUnmounted(() => { - window.clearInterval(intervalId); - }); +useInterval(fetch, 1000 * 60, { + immediate: true, + afterMounted: true, }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts index 9626d01619..9fdfe7f3e1 100644 --- a/packages/client/src/widgets/widget.ts +++ b/packages/client/src/widgets/widget.ts @@ -36,8 +36,9 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default: const mergeProps = () => { for (const prop of Object.keys(propsDef)) { - if (widgetProps.hasOwnProperty(prop)) continue; - widgetProps[prop] = propsDef[prop].default; + if (typeof widgetProps[prop] === 'undefined') { + widgetProps[prop] = propsDef[prop].default; + } } }; watch(widgetProps, () => { |