diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
| commit | 9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch) | |
| tree | ce5959571a981b9c4047da3c7b3fd080aa44222c /packages/client/src/widgets | |
| parent | wip: retention for dashboard (diff) | |
| download | sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/client/src/widgets')
33 files changed, 0 insertions, 3640 deletions
diff --git a/packages/client/src/widgets/activity.calendar.vue b/packages/client/src/widgets/activity.calendar.vue deleted file mode 100644 index 84f6af1c13..0000000000 --- a/packages/client/src/widgets/activity.calendar.vue +++ /dev/null @@ -1,81 +0,0 @@ -<template> -<svg viewBox="0 0 21 7"> - <rect v-for="record in activity" class="day" - width="1" height="1" - :x="record.x" :y="record.date.weekday" - rx="1" ry="1" - fill="transparent"> - <title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title> - </rect> - <rect v-for="record in activity" class="day" - :width="record.v" :height="record.v" - :x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)" - rx="1" ry="1" - :fill="record.color" - style="pointer-events: none;"/> - <rect class="today" - width="1" height="1" - :x="activity[0].x" :y="activity[0].date.weekday" - rx="1" ry="1" - fill="none" - stroke-width="0.1" - stroke="#f73520"/> -</svg> -</template> - -<script lang="ts" setup> -const props = defineProps<{ - activity: any[] -}>(); - -for (const d of props.activity) { - d.total = d.notes + d.replies + d.renotes; -} -const peak = Math.max(...props.activity.map(d => d.total)); - -const now = new Date(); -const year = now.getFullYear(); -const month = now.getMonth(); -const day = now.getDate(); - -let x = 20; -props.activity.slice().forEach((d, i) => { - d.x = x; - - const date = new Date(year, month, day - i); - d.date = { - year: date.getFullYear(), - month: date.getMonth(), - day: date.getDate(), - weekday: date.getDay(), - }; - - d.v = peak === 0 ? 0 : d.total / (peak / 2); - if (d.v > 1) d.v = 1; - const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170; - const cs = d.v * 100; - const cl = 15 + ((1 - d.v) * 80); - d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; - - if (d.date.weekday === 0) x--; -}); -</script> - -<style lang="scss" scoped> -svg { - display: block; - padding: 16px; - width: 100%; - box-sizing: border-box; - - > rect { - transform-origin: center; - - &.day { - &:hover { - fill: rgba(#000, 0.05); - } - } - } -} -</style> diff --git a/packages/client/src/widgets/activity.chart.vue b/packages/client/src/widgets/activity.chart.vue deleted file mode 100644 index b61e419f94..0000000000 --- a/packages/client/src/widgets/activity.chart.vue +++ /dev/null @@ -1,92 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown"> - <polyline - :points="pointsNote" - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - :points="pointsReply" - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - :points="pointsRenote" - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - :points="pointsTotal" - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> -</svg> -</template> - -<script lang="ts" setup> -const props = defineProps<{ - activity: any[] -}>(); - -let viewBoxX: number = $ref(147); -let viewBoxY: number = $ref(60); -let zoom: number = $ref(1); -let pos: number = $ref(0); -let pointsNote: any = $ref(null); -let pointsReply: any = $ref(null); -let pointsRenote: any = $ref(null); -let pointsTotal: any = $ref(null); - -function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); -} - -function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); -} - -function onMousedown(ev) { - const clickX = ev.clientX; - const clickY = ev.clientY; - const baseZoom = zoom; - const basePos = pos; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - clickX; - let moveTop = me.clientY - clickY; - - zoom = Math.max(1, baseZoom + (-moveTop / 20)); - pos = Math.min(0, basePos + moveLeft); - if (pos < -(((props.activity.length - 1) * zoom) - viewBoxX)) pos = -(((props.activity.length - 1) * zoom) - viewBoxX); - - render(); - }); -} - -function render() { - const peak = Math.max(...props.activity.map(d => d.total)); - if (peak !== 0) { - const activity = props.activity.slice().reverse(); - pointsNote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.notes / peak)) * viewBoxY}`).join(' '); - pointsReply = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.replies / peak)) * viewBoxY}`).join(' '); - pointsRenote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.renotes / peak)) * viewBoxY}`).join(' '); - pointsTotal = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.total / peak)) * viewBoxY}`).join(' '); - } -} -</script> - -<style lang="scss" scoped> -svg { - display: block; - padding: 16px; - width: 100%; - box-sizing: border-box; - cursor: all-scroll; -} -</style> diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue deleted file mode 100644 index 238a05ca09..0000000000 --- a/packages/client/src/widgets/activity.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" class="mkw-activity"> - <template #header><i class="ti ti-chart-line"></i>{{ i18n.ts._widgets.activity }}</template> - <template #func><button class="_button" @click="toggleView()"><i class="ti ti-selector"></i></button></template> - - <div> - <MkLoading v-if="fetching"/> - <template v-else> - <XCalendar v-show="widgetProps.view === 0" :activity="[].concat(activity)"/> - <XChart v-show="widgetProps.view === 1" :activity="[].concat(activity)"/> - </template> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -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/MkContainer.vue'; -import { $i } from '@/account'; -import { i18n } from '@/i18n'; - -const name = 'activity'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, - transparent: { - type: 'boolean' as const, - default: false, - }, - view: { - type: 'number' as const, - default: 0, - hidden: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: '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.apiGet('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 deleted file mode 100644 index 828490fd9c..0000000000 --- a/packages/client/src/widgets/aichan.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> -<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-aichan"> - <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" setup> -import { onMounted, onUnmounted, reactive, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; - -const name = 'ai'; - -const widgetPropsDef = { - transparent: { - type: 'boolean' as const, - default: false, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -const live2d = ref<HTMLIFrameElement>(); - -const touched = () => { - //if (this.live2d) this.live2d.changeExpression('gurugurume'); -}; - -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); -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.dedjhjmo { - width: 100%; - height: 350px; - border: none; - pointer-events: none; -} -</style> diff --git a/packages/client/src/widgets/aiscript.vue b/packages/client/src/widgets/aiscript.vue deleted file mode 100644 index 4009edb8b8..0000000000 --- a/packages/client/src/widgets/aiscript.vue +++ /dev/null @@ -1,175 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscript"> - <template #header><i class="ti ti-terminal-2"></i>{{ i18n.ts._widgets.aiscript }}</template> - - <div class="uylguesu _monospace"> - <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> - </div> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref, watch } from 'vue'; -import { AiScript, parse, utils } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import MkContainer from '@/components/MkContainer.vue'; -import { createAiScriptEnv } from '@/scripts/aiscript/api'; -import { $i } from '@/account'; -import { i18n } from '@/i18n'; - -const name = 'aiscript'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, - script: { - type: 'string' as const, - multiline: true, - default: '(1 + 1)', - 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<{ (ev: '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 (err) { - os.alert({ - type: 'error', - text: 'Syntax error :(', - }); - return; - } - try { - await aiscript.exec(ast); - } catch (err) { - os.alert({ - type: 'error', - text: err, - }); - } -}; - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.uylguesu { - text-align: right; - - > textarea { - display: block; - width: 100%; - max-width: 100%; - min-width: 100%; - padding: 16px; - color: var(--fg); - background: transparent; - border: none; - border-bottom: solid 0.5px var(--divider); - border-radius: 0; - box-sizing: border-box; - font: inherit; - - &:focus-visible { - outline: none; - } - } - - > button { - display: inline-block; - margin: 8px; - padding: 0 10px; - height: 28px; - outline: none; - border-radius: 4px; - - &:disabled { - opacity: 0.7; - cursor: default; - } - } - - > .logs { - border-top: solid 0.5px var(--divider); - text-align: left; - padding: 16px; - - &:empty { - display: none; - } - - > .log { - &:not(.print) { - opacity: 0.7; - } - } - } -} -</style> diff --git a/packages/client/src/widgets/button.vue b/packages/client/src/widgets/button.vue deleted file mode 100644 index f0148d7f4e..0000000000 --- a/packages/client/src/widgets/button.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<div class="mkw-button"> - <MkButton :primary="widgetProps.colored" full @click="run"> - {{ widgetProps.label }} - </MkButton> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref, watch } from 'vue'; -import { AiScript, parse, utils } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import { createAiScriptEnv } from '@/scripts/aiscript/api'; -import { $i } from '@/account'; -import MkButton from '@/components/MkButton.vue'; - -const name = 'button'; - -const widgetPropsDef = { - label: { - type: 'string' as const, - default: 'BUTTON', - }, - colored: { - type: 'boolean' as const, - default: true, - }, - script: { - type: 'string' as const, - multiline: true, - default: 'Mk:dialog("hello" "world")', - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: '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 (err) { - os.alert({ - type: 'error', - text: 'Syntax error :(', - }); - return; - } - try { - await aiscript.exec(ast); - } catch (err) { - os.alert({ - type: 'error', - text: err, - }); - } -}; - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.mkw-button { -} -</style> diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue deleted file mode 100644 index 99bd36e2fc..0000000000 --- a/packages/client/src/widgets/calendar.vue +++ /dev/null @@ -1,213 +0,0 @@ -<template> -<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> - <span class="month">{{ $t('monthX', { month }) }}</span> - </p> - <p v-if="month === 1 && day === 1" class="day">🎉{{ $t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p> - <p v-else class="day">{{ $t('dayX', { day }) }}</p> - <p class="week-day">{{ weekDay }}</p> - </div> - <div class="info"> - <div> - <p>{{ i18n.ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${dayP}%` }"></div> - </div> - </div> - <div> - <p>{{ i18n.ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${monthP}%` }"></div> - </div> - </div> - <div> - <p>{{ i18n.ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${yearP}%` }"></div> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts" setup> -import { onUnmounted, ref } from 'vue'; -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'; - -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, -); - -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.ts._weekday.sunday, - i18n.ts._weekday.monday, - i18n.ts._weekday.tuesday, - i18n.ts._weekday.wednesday, - i18n.ts._weekday.thursday, - i18n.ts._weekday.friday, - 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 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; -}; - -useInterval(tick, 1000, { - immediate: true, - afterMounted: false, -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.mkw-calendar { - padding: 16px 0; - - &:after { - content: ""; - display: block; - clear: both; - } - - > .calendar { - float: left; - width: 60%; - text-align: center; - - &.isHoliday { - > .day { - color: #ef95a0; - } - } - - > .month-and-year, > .week-day { - margin: 0; - line-height: 18px; - font-size: 0.9em; - - > .year, > .month { - margin: 0 4px; - } - } - - > .day { - margin: 10px 0; - line-height: 32px; - font-size: 1.75em; - } - } - - > .info { - display: block; - float: left; - width: 40%; - padding: 0 16px 0 0; - box-sizing: border-box; - - > div { - margin-bottom: 8px; - - &:last-child { - margin-bottom: 4px; - } - - > p { - margin: 0 0 2px 0; - font-size: 0.75em; - line-height: 18px; - opacity: 0.8; - - > b { - margin-left: 2px; - } - } - - > .meter { - width: 100%; - overflow: hidden; - background: var(--X11); - border-radius: 8px; - - > .val { - height: 4px; - transition: width .3s cubic-bezier(0.23, 1, 0.32, 1); - } - } - - &:nth-child(1) { - > .meter > .val { - background: #f7796c; - } - } - - &:nth-child(2) { - > .meter > .val { - background: #a1de41; - } - } - - &:nth-child(3) { - > .meter > .val { - background: #41ddde; - } - } - } - } -} -</style> diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue deleted file mode 100644 index dc99b6631e..0000000000 --- a/packages/client/src/widgets/clock.vue +++ /dev/null @@ -1,203 +0,0 @@ -<template> -<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock"> - <div class="vubelbmv" :class="widgetProps.size"> - <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace label a abbrev">{{ tzAbbrev }}</div> - <MkAnalogClock - class="clock" - :thickness="widgetProps.thickness" - :offset="tzOffset" - :graduations="widgetProps.graduations" - :fade-graduations="widgetProps.fadeGraduations" - :twentyfour="widgetProps.twentyFour" - :s-animation="widgetProps.sAnimation" - /> - <MkDigitalClock v-if="widgetProps.label === 'time' || widgetProps.label === 'timeAndTz'" class="_monospace label c time" :show-s="false" :offset="tzOffset"/> - <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace label d offset">{{ tzOffsetLabel }}</div> - </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/MkContainer.vue'; -import MkAnalogClock from '@/components/MkAnalogClock.vue'; -import MkDigitalClock from '@/components/MkDigitalClock.vue'; -import { timezones } from '@/scripts/timezones'; -import { i18n } from '@/i18n'; - -const name = 'clock'; - -const widgetPropsDef = { - transparent: { - type: 'boolean' as const, - default: false, - }, - size: { - type: 'radio' as const, - default: 'medium', - options: [{ - value: 'small', label: i18n.ts.small, - }, { - value: 'medium', label: i18n.ts.medium, - }, { - value: 'large', label: i18n.ts.large, - }], - }, - thickness: { - type: 'radio' as const, - default: 0.2, - options: [{ - value: 0.1, label: 'thin', - }, { - value: 0.2, label: 'medium', - }, { - value: 0.3, label: 'thick', - }], - }, - graduations: { - type: 'radio' as const, - default: 'numbers', - options: [{ - value: 'none', label: 'None', - }, { - value: 'dots', label: 'Dots', - }, { - value: 'numbers', label: 'Numbers', - }], - }, - fadeGraduations: { - type: 'boolean' as const, - default: true, - }, - sAnimation: { - type: 'radio' as const, - default: 'elastic', - options: [{ - value: 'none', label: 'None', - }, { - value: 'elastic', label: 'Elastic', - }, { - value: 'easeOut', label: 'Ease out', - }], - }, - twentyFour: { - type: 'boolean' as const, - default: false, - }, - label: { - type: 'radio' as const, - default: 'none', - options: [{ - value: 'none', label: 'None', - }, { - value: 'time', label: 'Time', - }, { - value: 'tz', label: 'TZ', - }, { - value: 'timeAndTz', label: 'Time + TZ', - }], - }, - timezone: { - type: 'enum' as const, - default: null, - enum: [...timezones.map((tz) => ({ - label: tz.name, - value: tz.name.toLowerCase(), - })), { - label: '(auto)', - value: null, - }], - }, -}; - -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 tzAbbrev = $computed(() => (widgetProps.timezone === null - ? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev - : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?'); - -const tzOffset = $computed(() => widgetProps.timezone === null - ? 0 - new Date().getTimezoneOffset() - : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0); - -const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0')); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.vubelbmv { - position: relative; - - > .label { - position: absolute; - opacity: 0.7; - - &.a { - top: 14px; - left: 14px; - } - - &.b { - top: 14px; - right: 14px; - } - - &.c { - bottom: 14px; - left: 14px; - } - - &.d { - bottom: 14px; - right: 14px; - } - } - - > .clock { - margin: auto; - } - - &.small { - padding: 12px; - - > .clock { - height: 100px; - } - } - - &.medium { - padding: 14px; - - > .clock { - height: 150px; - } - } - - &.large { - padding: 16px; - - > .clock { - height: 200px; - } - } -} -</style> diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue deleted file mode 100644 index d2bfd523f3..0000000000 --- a/packages/client/src/widgets/digital-clock.vue +++ /dev/null @@ -1,92 +0,0 @@ -<template> -<div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }"> - <div v-if="widgetProps.showLabel" class="label">{{ tzAbbrev }}</div> - <div class="time"> - <MkDigitalClock :show-ms="widgetProps.showMs" :offset="tzOffset"/> - </div> - <div v-if="widgetProps.showLabel" class="label">{{ tzOffsetLabel }}</div> -</div> -</template> - -<script lang="ts" setup> -import { onUnmounted, ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import { timezones } from '@/scripts/timezones'; -import MkDigitalClock from '@/components/MkDigitalClock.vue'; - -const name = 'digitalClock'; - -const widgetPropsDef = { - transparent: { - type: 'boolean' as const, - default: false, - }, - fontSize: { - type: 'number' as const, - default: 1.5, - step: 0.1, - }, - showMs: { - type: 'boolean' as const, - default: true, - }, - showLabel: { - type: 'boolean' as const, - default: true, - }, - timezone: { - type: 'enum' as const, - default: null, - enum: [...timezones.map((tz) => ({ - label: tz.name, - value: tz.name.toLowerCase(), - })), { - label: '(auto)', - value: null, - }], - }, -}; - -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 tzAbbrev = $computed(() => (widgetProps.timezone === null - ? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev - : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?'); - -const tzOffset = $computed(() => widgetProps.timezone === null - ? 0 - new Date().getTimezoneOffset() - : timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0); - -const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0')); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.mkw-digitalClock { - padding: 16px 0; - text-align: center; - - > .label { - font-size: 65%; - opacity: 0.7; - } -} -</style> diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue deleted file mode 100644 index 3374783b0c..0000000000 --- a/packages/client/src/widgets/federation.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" class="mkw-federation"> - <template #header><i class="ti ti-whirl"></i>{{ i18n.ts._widgets.federation }}</template> - - <div class="wbrkwalb"> - <MkLoading v-if="fetching"/> - <transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances"> - <div v-for="(instance, i) in instances" :key="instance.id" class="instance"> - <img :src="getInstanceIcon(instance)" alt=""/> - <div class="body"> - <a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a> - <p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p> - </div> - <MkMiniChart class="chart" :src="charts[i].requests.received"/> - </div> - </transition-group> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import MkContainer from '@/components/MkContainer.vue'; -import MkMiniChart from '@/components/MkMiniChart.vue'; -import * as os from '@/os'; -import { useInterval } from '@/scripts/use-interval'; -import { i18n } from '@/i18n'; -import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; - -const name = 'federation'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps> & { foldable?: boolean; scrollable?: boolean; }>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; foldable?: boolean; scrollable?: boolean; }>(); -const emit = defineEmits<{ (ev: '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 fetchedInstances = await os.api('federation/instances', { - sort: '+lastCommunicatedAt', - limit: 5, - }); - 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; -}; - -useInterval(fetch, 1000 * 60, { - immediate: true, - afterMounted: true, -}); - -function getInstanceIcon(instance): string { - return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png'; -} - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.wbrkwalb { - $bodyTitleHieght: 18px; - $bodyInfoHieght: 16px; - - height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px; - overflow: hidden; - - > .instances { - .chart-move { - transition: transform 1s ease; - } - - > .instance { - display: flex; - align-items: center; - padding: 14px 16px; - border-bottom: solid 0.5px var(--divider); - - > img { - display: block; - width: ($bodyTitleHieght + $bodyInfoHieght); - height: ($bodyTitleHieght + $bodyInfoHieght); - object-fit: cover; - border-radius: 4px; - margin-right: 8px; - } - - > .body { - flex: 1; - overflow: hidden; - font-size: 0.9em; - color: var(--fg); - padding-right: 8px; - - > .a { - display: block; - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: $bodyTitleHieght; - } - - > p { - margin: 0; - font-size: 75%; - opacity: 0.7; - line-height: $bodyInfoHieght; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - > .chart { - height: 30px; - } - } - } -} -</style> diff --git a/packages/client/src/widgets/index.ts b/packages/client/src/widgets/index.ts deleted file mode 100644 index 39826f13c8..0000000000 --- a/packages/client/src/widgets/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { App, defineAsyncComponent } from 'vue'; - -export default function(app: App) { - app.component('MkwMemo', defineAsyncComponent(() => import('./memo.vue'))); - app.component('MkwNotifications', defineAsyncComponent(() => import('./notifications.vue'))); - 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'))); - app.component('MkwPhotos', defineAsyncComponent(() => import('./photos.vue'))); - app.component('MkwDigitalClock', defineAsyncComponent(() => import('./digital-clock.vue'))); - app.component('MkwUnixClock', defineAsyncComponent(() => import('./unix-clock.vue'))); - app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue'))); - app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue'))); - app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue'))); - 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'))); - app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue'))); -} - -export const widgets = [ - 'memo', - 'notifications', - 'timeline', - 'calendar', - 'rss', - 'rssTicker', - 'trends', - 'clock', - 'activity', - 'photos', - 'digitalClock', - 'unixClock', - 'federation', - 'instanceCloud', - 'postForm', - 'slideshow', - 'serverMetric', - 'onlineUsers', - 'jobQueue', - 'button', - 'aiscript', - 'aichan', - 'userList', -]; diff --git a/packages/client/src/widgets/instance-cloud.vue b/packages/client/src/widgets/instance-cloud.vue deleted file mode 100644 index 4965616995..0000000000 --- a/packages/client/src/widgets/instance-cloud.vue +++ /dev/null @@ -1,81 +0,0 @@ -<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="getInstanceIcon(instance)"> - </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/MkContainer.vue'; -import MkTagCloud from '@/components/MkTagCloud.vue'; -import * as os from '@/os'; -import { useInterval } from '@/scripts/use-interval'; -import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; - -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, -}); - -function getInstanceIcon(instance): string { - return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png'; -} - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue deleted file mode 100644 index 9f19c51825..0000000000 --- a/packages/client/src/widgets/job-queue.vue +++ /dev/null @@ -1,197 +0,0 @@ -<template> -<div class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }"> - <div class="inbox"> - <div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ti ti-alert-triangle icon"></i></div> - <div class="values"> - <div> - <div>Process</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: 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: 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: 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="current.deliver.waiting > 0" class="ti ti-alert-triangle icon"></i></div> - <div class="values"> - <div> - <div>Process</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: 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: 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: 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" setup> -import { onMounted, onUnmounted, reactive, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import { stream } from '@/stream'; -import number from '@/filters/number'; -import * as sound from '@/scripts/sound'; -import * as os from '@/os'; -import { deepClone } from '@/scripts/clone'; - -const name = 'jobQueue'; - -const widgetPropsDef = { - transparent: { - type: 'boolean' as const, - default: false, - }, - sound: { - type: 'boolean' as const, - default: false, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -const connection = stream.useChannel('queueStats'); -const current = reactive({ - inbox: { - activeSincePrevTick: 0, - active: 0, - waiting: 0, - delayed: 0, - }, - deliver: { - activeSincePrevTick: 0, - active: 0, - waiting: 0, - delayed: 0, - }, -}); -const prev = reactive({} as typeof current); -const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1); - -for (const domain of ['inbox', 'deliver']) { - prev[domain] = deepClone(current[domain]); -} - -const onStats = (stats) => { - for (const domain of ['inbox', 'deliver']) { - prev[domain] = deepClone(current[domain]); - current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick; - current[domain].active = stats[domain].active; - current[domain].waiting = stats[domain].waiting; - current[domain].delayed = stats[domain].delayed; - - if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) { - jammedSound.play(); - } - } -}; - -const onStatsLog = (statsLog) => { - for (const stats of [...statsLog].reverse()) { - onStats(stats); - } -}; - -connection.on('stats', onStats); -connection.on('statsLog', onStatsLog); - -connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 1, -}); - -onUnmounted(() => { - connection.off('stats', onStats); - connection.off('statsLog', onStatsLog); - connection.dispose(); -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -@keyframes warnBlink { - 0% { opacity: 1; } - 50% { opacity: 0; } -} - -.mkw-jobQueue { - font-size: 0.9em; - - > div { - padding: 16px; - - &:not(:first-child) { - border-top: solid 0.5px var(--divider); - } - - > .label { - display: flex; - - > .icon { - color: var(--warn); - margin-left: auto; - animation: warnBlink 1s infinite; - } - } - - > .values { - display: flex; - - > div { - flex: 1; - - > div:first-child { - opacity: 0.7; - } - - > div:last-child { - &.inc { - color: var(--warn); - } - - &.dec { - color: var(--success); - } - } - } - } - } -} -</style> diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue deleted file mode 100644 index 1cc0e10bba..0000000000 --- a/packages/client/src/widgets/memo.vue +++ /dev/null @@ -1,111 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-memo"> - <template #header><i class="ti ti-note"></i>{{ i18n.ts._widgets.memo }}</template> - - <div class="otgbylcu"> - <textarea v-model="text" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea> - <button :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import MkContainer from '@/components/MkContainer.vue'; -import { defaultStore } from '@/store'; -import { i18n } from '@/i18n'; - -const name = 'memo'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -const text = ref<string | null>(defaultStore.state.memo); -const changed = ref(false); -let timeoutId; - -const saveMemo = () => { - defaultStore.set('memo', text.value); - changed.value = false; -}; - -const onChange = () => { - changed.value = true; - window.clearTimeout(timeoutId); - timeoutId = window.setTimeout(saveMemo, 1000); -}; - -watch(() => defaultStore.reactiveState.memo, newText => { - text.value = newText.value; -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.otgbylcu { - padding-bottom: 28px + 16px; - - > textarea { - display: block; - width: 100%; - max-width: 100%; - min-width: 100%; - padding: 16px; - color: var(--fg); - background: transparent; - border: none; - border-bottom: solid 0.5px var(--divider); - border-radius: 0; - box-sizing: border-box; - font: inherit; - font-size: 0.9em; - - &:focus-visible { - outline: none; - } - } - - > button { - display: block; - position: absolute; - bottom: 8px; - right: 8px; - margin: 0; - padding: 0 10px; - height: 28px; - outline: none; - border-radius: 4px; - - &:disabled { - opacity: 0.7; - cursor: default; - } - } -} -</style> diff --git a/packages/client/src/widgets/notifications.vue b/packages/client/src/widgets/notifications.vue deleted file mode 100644 index e697209444..0000000000 --- a/packages/client/src/widgets/notifications.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true" class="mkw-notifications"> - <template #header><i class="ti ti-bell"></i>{{ i18n.ts.notifications }}</template> - <template #func><button class="_button" @click="configureNotification()"><i class="ti ti-settings"></i></button></template> - - <div> - <XNotifications :include-types="widgetProps.includingTypes"/> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import MkContainer from '@/components/MkContainer.vue'; -import XNotifications from '@/components/MkNotifications.vue'; -import * as os from '@/os'; -import { i18n } from '@/i18n'; - -const name = 'notifications'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, - height: { - type: 'number' as const, - default: 300, - }, - includingTypes: { - type: 'array' as const, - hidden: true, - default: null, - }, -}; - -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, save } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -const configureNotification = () => { - os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.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 deleted file mode 100644 index e9ab79b111..0000000000 --- a/packages/client/src/widgets/online-users.vue +++ /dev/null @@ -1,78 +0,0 @@ -<template> -<div class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }"> - <I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" text-tag="span" class="text"> - <template #n><b>{{ onlineUsersCount }}</b></template> - </I18n> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import { useInterval } from '@/scripts/use-interval'; -import { i18n } from '@/i18n'; - -const name = 'onlineUsers'; - -const widgetPropsDef = { - transparent: { - type: 'boolean' as const, - default: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: '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; - }); -}; - -useInterval(tick, 1000 * 15, { - immediate: true, - afterMounted: true, -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.mkw-onlineUsers { - text-align: center; - - &.pad { - padding: 16px 0; - } - - > .text { - ::v-deep(b) { - color: #41b781; - } - - ::v-deep(span) { - opacity: 0.7; - } - } -} -</style> diff --git a/packages/client/src/widgets/photos.vue b/packages/client/src/widgets/photos.vue deleted file mode 100644 index 4ad5324053..0000000000 --- a/packages/client/src/widgets/photos.vue +++ /dev/null @@ -1,123 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" class="mkw-photos"> - <template #header><i class="ti ti-camera"></i>{{ i18n.ts._widgets.photos }}</template> - - <div class=""> - <MkLoading v-if="fetching"/> - <div v-else :class="$style.stream"> - <div - v-for="(image, i) in images" :key="i" - :class="$style.img" - :style="`background-image: url(${thumbnail(image)})`" - ></div> - </div> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, reactive, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import { stream } from '@/stream'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; -import * as os from '@/os'; -import MkContainer from '@/components/MkContainer.vue'; -import { defaultStore } from '@/store'; -import { i18n } from '@/i18n'; - -const name = 'photos'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, - transparent: { - type: 'boolean' as const, - default: false, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -const connection = stream.useChannel('main'); -const images = ref([]); -const fetching = ref(true); - -const onDriveFileCreated = (file) => { - if (/^image\/.+$/.test(file.type)) { - images.value.unshift(file); - if (images.value.length > 9) images.value.pop(); - } -}; - -const thumbnail = (image: any): string => { - return defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; -}; - -os.api('drive/stream', { - type: 'image/*', - limit: 9, -}).then(res => { - images.value = res; - fetching.value = false; -}); - -connection.on('driveFileCreated', onDriveFileCreated); -onUnmounted(() => { - connection.dispose(); -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" module> -.root[data-transparent] { - .stream { - padding: 0; - } - - .img { - border: solid 4px transparent; - border-radius: 8px; - } -} - -.stream { - display: flex; - justify-content: center; - flex-wrap: wrap; - padding: 8px; - - .img { - flex: 1 1 33%; - width: 33%; - height: 80px; - box-sizing: border-box; - background-position: center center; - background-size: cover; - background-clip: content-box; - border: solid 2px transparent; - border-radius: 4px; - } -} -</style> diff --git a/packages/client/src/widgets/post-form.vue b/packages/client/src/widgets/post-form.vue deleted file mode 100644 index f1708775ba..0000000000 --- a/packages/client/src/widgets/post-form.vue +++ /dev/null @@ -1,35 +0,0 @@ -<template> -<XPostForm class="_panel mkw-postForm" :fixed="true" :autofocus="false"/> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import { GetFormResultType } from '@/scripts/form'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import XPostForm from '@/components/MkPostForm.vue'; - -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<{ (ev: '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/rss-ticker.vue b/packages/client/src/widgets/rss-ticker.vue deleted file mode 100644 index 44c21d1836..0000000000 --- a/packages/client/src/widgets/rss-ticker.vue +++ /dev/null @@ -1,152 +0,0 @@ -<template> -<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-rss-ticker"> - <template #header><i class="ti ti-rss"></i>RSS</template> - <template #func><button class="_button" @click="configure"><i class="ti ti-settings"></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/MkMarquee.vue'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import MkContainer from '@/components/MkContainer.vue'; -import { useInterval } from '@/scripts/use-interval'; -import { shuffle } from '@/scripts/shuffle'; - -const name = 'rssTicker'; - -const widgetPropsDef = { - url: { - type: 'string' as const, - default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', - }, - shuffle: { - type: 'boolean' as const, - default: true, - }, - 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 = () => { - window.fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => { - res.json().then(feed => { - if (widgetProps.shuffle) { - shuffle(feed.items); - } - 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 deleted file mode 100644 index c0338c8e47..0000000000 --- a/packages/client/src/widgets/rss.vue +++ /dev/null @@ -1,96 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-rss"> - <template #header><i class="ti ti-rss"></i>RSS</template> - <template #func><button class="_button" @click="configure"><i class="ti ti-settings"></i></button></template> - - <div class="ekmkgxbj"> - <MkLoading v-if="fetching"/> - <div v-else class="feed"> - <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> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import MkContainer from '@/components/MkContainer.vue'; -import { useInterval } from '@/scripts/use-interval'; - -const name = 'rss'; - -const widgetPropsDef = { - 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>; - -// 現時点では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); - -const tick = () => { - window.fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => { - res.json().then(feed => { - items.value = feed.items; - fetching.value = false; - }); - }); -}; - -watch(() => widgetProps.url, tick); - -useInterval(tick, 60000, { - immediate: true, - afterMounted: true, -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.ekmkgxbj { - > .feed { - padding: 0; - font-size: 0.9em; - - > .item { - display: block; - padding: 8px 16px; - color: var(--fg); - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - - &:nth-child(even) { - background: rgba(#000, 0.05); - } - } - } -} -</style> diff --git a/packages/client/src/widgets/server-metric/cpu-mem.vue b/packages/client/src/widgets/server-metric/cpu-mem.vue deleted file mode 100644 index 80a8e427e1..0000000000 --- a/packages/client/src/widgets/server-metric/cpu-mem.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="lcfyofjk"> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <defs> - <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="cpuPolygonPoints" - fill="#fff" - fill-opacity="0.5" - /> - <polyline - :points="cpuPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1" - /> - <circle - :cx="cpuHeadX" - :cy="cpuHeadY" - r="1.5" - fill="#fff" - /> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`" - /> - <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> - </svg> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <defs> - <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="memPolygonPoints" - fill="#fff" - fill-opacity="0.5" - /> - <polyline - :points="memPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1" - /> - <circle - :cx="memHeadX" - :cy="memHeadY" - r="1.5" - fill="#fff" - /> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`" - /> - <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> - </svg> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; -import { v4 as uuid } from 'uuid'; - -const props = defineProps<{ - connection: any, - meta: any -}>(); - -let viewBoxX: number = $ref(50); -let viewBoxY: number = $ref(30); -let stats: any[] = $ref([]); -const cpuGradientId = uuid(); -const cpuMaskId = uuid(); -const memGradientId = uuid(); -const memMaskId = uuid(); -let cpuPolylinePoints: string = $ref(''); -let memPolylinePoints: string = $ref(''); -let cpuPolygonPoints: string = $ref(''); -let memPolygonPoints: string = $ref(''); -let cpuHeadX: any = $ref(null); -let cpuHeadY: any = $ref(null); -let memHeadX: any = $ref(null); -let memHeadY: any = $ref(null); -let cpuP: string = $ref(''); -let memP: string = $ref(''); - -onMounted(() => { - props.connection.on('stats', onStats); - props.connection.on('statsLog', onStatsLog); - props.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - }); -}); - -onBeforeUnmount(() => { - props.connection.off('stats', onStats); - props.connection.off('statsLog', onStatsLog); -}); - -function onStats(connStats) { - stats.push(connStats); - if (stats.length > 50) stats.shift(); - - let cpuPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - s.cpu) * viewBoxY]); - let memPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY]); - cpuPolylinePoints = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - memPolylinePoints = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`; - memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`; - - cpuHeadX = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][0]; - cpuHeadY = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][1]; - memHeadX = memPolylinePointsStats[memPolylinePointsStats.length - 1][0]; - memHeadY = memPolylinePointsStats[memPolylinePointsStats.length - 1][1]; - - cpuP = (connStats.cpu * 100).toFixed(0); - memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); -} - -function onStatsLog(statsLog) { - for (const revStats of [...statsLog].reverse()) { - onStats(revStats); - } -} -</script> - -<style lang="scss" scoped> -.lcfyofjk { - display: flex; - - > svg { - display: block; - padding: 10px; - width: 50%; - - &:first-child { - padding-right: 5px; - } - - &:last-child { - padding-left: 5px; - } - - > text { - font-size: 4.5px; - fill: currentColor; - - > tspan { - opacity: 0.5; - } - } - } -} -</style> diff --git a/packages/client/src/widgets/server-metric/cpu.vue b/packages/client/src/widgets/server-metric/cpu.vue deleted file mode 100644 index e7b2226d1f..0000000000 --- a/packages/client/src/widgets/server-metric/cpu.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div class="vrvdvrys"> - <XPie class="pie" :value="usage"/> - <div> - <p><i class="ti ti-cpu"></i>CPU</p> - <p>{{ meta.cpu.cores }} Logical cores</p> - <p>{{ meta.cpu.model }}</p> - </div> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; -import XPie from './pie.vue'; - -const props = defineProps<{ - connection: any, - meta: any -}>(); - -let usage: number = $ref(0); - -function onStats(stats) { - usage = stats.cpu; -} - -onMounted(() => { - props.connection.on('stats', onStats); -}); - -onBeforeUnmount(() => { - props.connection.off('stats', onStats); -}); -</script> - -<style lang="scss" scoped> -.vrvdvrys { - display: flex; - padding: 16px; - - > .pie { - height: 82px; - flex-shrink: 0; - margin-right: 16px; - } - - > div { - flex: 1; - - > p { - margin: 0; - font-size: 0.8em; - - &:first-child { - font-weight: bold; - margin-bottom: 4px; - - > i { - margin-right: 4px; - } - } - } - } -} -</style> diff --git a/packages/client/src/widgets/server-metric/disk.vue b/packages/client/src/widgets/server-metric/disk.vue deleted file mode 100644 index 3d22d05383..0000000000 --- a/packages/client/src/widgets/server-metric/disk.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<div class="zbwaqsat"> - <XPie class="pie" :value="usage"/> - <div> - <p><i class="ti ti-database"></i>Disk</p> - <p>Total: {{ bytes(total, 1) }}</p> - <p>Free: {{ bytes(available, 1) }}</p> - <p>Used: {{ bytes(used, 1) }}</p> - </div> -</div> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import XPie from './pie.vue'; -import bytes from '@/filters/bytes'; - -const props = defineProps<{ - meta: any; // TODO -}>(); - -const usage = $computed(() => props.meta.fs.used / props.meta.fs.total); -const total = $computed(() => props.meta.fs.total); -const used = $computed(() => props.meta.fs.used); -const available = $computed(() => props.meta.fs.total - props.meta.fs.used); -</script> - -<style lang="scss" scoped> -.zbwaqsat { - display: flex; - padding: 16px; - - > .pie { - height: 82px; - flex-shrink: 0; - margin-right: 16px; - } - - > div { - flex: 1; - - > p { - margin: 0; - font-size: 0.8em; - - &:first-child { - font-weight: bold; - margin-bottom: 4px; - - > i { - margin-right: 4px; - } - } - } - } -} -</style> diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue deleted file mode 100644 index bc3fca6fc1..0000000000 --- a/packages/client/src/widgets/server-metric/index.vue +++ /dev/null @@ -1,87 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent"> - <template #header><i class="ti ti-server"></i>{{ i18n.ts._widgets.serverMetric }}</template> - <template #func><button class="_button" @click="toggleView()"><i class="ti ti-selector"></i></button></template> - - <div v-if="meta" class="mkw-serverMetric"> - <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" setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget'; -import XCpuMemory from './cpu-mem.vue'; -import XNet from './net.vue'; -import XCpu from './cpu.vue'; -import XMemory from './mem.vue'; -import XDisk from './disk.vue'; -import MkContainer from '@/components/MkContainer.vue'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import { stream } from '@/stream'; -import { i18n } from '@/i18n'; - -const name = 'serverMetric'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, - transparent: { - type: 'boolean' as const, - default: false, - }, - view: { - type: 'number' as const, - default: 0, - hidden: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: '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/server-metric/mem.vue b/packages/client/src/widgets/server-metric/mem.vue deleted file mode 100644 index 6018eb4265..0000000000 --- a/packages/client/src/widgets/server-metric/mem.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<div class="zlxnikvl"> - <XPie class="pie" :value="usage"/> - <div> - <p><i class="fas fa-memory"></i>RAM</p> - <p>Total: {{ bytes(total, 1) }}</p> - <p>Used: {{ bytes(used, 1) }}</p> - <p>Free: {{ bytes(free, 1) }}</p> - </div> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; -import XPie from './pie.vue'; -import bytes from '@/filters/bytes'; - -const props = defineProps<{ - connection: any, - meta: any -}>(); - -let usage: number = $ref(0); -let total: number = $ref(0); -let used: number = $ref(0); -let free: number = $ref(0); - -function onStats(stats) { - usage = stats.mem.active / props.meta.mem.total; - total = props.meta.mem.total; - used = stats.mem.active; - free = total - used; -} - -onMounted(() => { - props.connection.on('stats', onStats); -}); - -onBeforeUnmount(() => { - props.connection.off('stats', onStats); -}); -</script> - -<style lang="scss" scoped> -.zlxnikvl { - display: flex; - padding: 16px; - - > .pie { - height: 82px; - flex-shrink: 0; - margin-right: 16px; - } - - > div { - flex: 1; - - > p { - margin: 0; - font-size: 0.8em; - - &:first-child { - font-weight: bold; - margin-bottom: 4px; - - > i { - margin-right: 4px; - } - } - } - } -} -</style> diff --git a/packages/client/src/widgets/server-metric/net.vue b/packages/client/src/widgets/server-metric/net.vue deleted file mode 100644 index ab8b0fe471..0000000000 --- a/packages/client/src/widgets/server-metric/net.vue +++ /dev/null @@ -1,140 +0,0 @@ -<template> -<div class="oxxrhrto"> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polygon - :points="inPolygonPoints" - fill="#94a029" - fill-opacity="0.5" - /> - <polyline - :points="inPolylinePoints" - fill="none" - stroke="#94a029" - stroke-width="1" - /> - <circle - :cx="inHeadX" - :cy="inHeadY" - r="1.5" - fill="#94a029" - /> - <text x="1" y="5">NET rx <tspan>{{ bytes(inRecent) }}</tspan></text> - </svg> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polygon - :points="outPolygonPoints" - fill="#ff9156" - fill-opacity="0.5" - /> - <polyline - :points="outPolylinePoints" - fill="none" - stroke="#ff9156" - stroke-width="1" - /> - <circle - :cx="outHeadX" - :cy="outHeadY" - r="1.5" - fill="#ff9156" - /> - <text x="1" y="5">NET tx <tspan>{{ bytes(outRecent) }}</tspan></text> - </svg> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; -import bytes from '@/filters/bytes'; - -const props = defineProps<{ - connection: any, - meta: any -}>(); - -let viewBoxX: number = $ref(50); -let viewBoxY: number = $ref(30); -let stats: any[] = $ref([]); -let inPolylinePoints: string = $ref(''); -let outPolylinePoints: string = $ref(''); -let inPolygonPoints: string = $ref(''); -let outPolygonPoints: string = $ref(''); -let inHeadX: any = $ref(null); -let inHeadY: any = $ref(null); -let outHeadX: any = $ref(null); -let outHeadY: any = $ref(null); -let inRecent: number = $ref(0); -let outRecent: number = $ref(0); - -onMounted(() => { - props.connection.on('stats', onStats); - props.connection.on('statsLog', onStatsLog); - props.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - }); -}); - -onBeforeUnmount(() => { - props.connection.off('stats', onStats); - props.connection.off('statsLog', onStatsLog); -}); - -function onStats(connStats) { - stats.push(connStats); - if (stats.length > 50) stats.shift(); - - const inPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.rx))); - const outPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.tx))); - - let inPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY]); - let outPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY]); - inPolylinePoints = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - outPolylinePoints = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`; - outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`; - - inHeadX = inPolylinePointsStats[inPolylinePointsStats.length - 1][0]; - inHeadY = inPolylinePointsStats[inPolylinePointsStats.length - 1][1]; - outHeadX = outPolylinePointsStats[outPolylinePointsStats.length - 1][0]; - outHeadY = outPolylinePointsStats[outPolylinePointsStats.length - 1][1]; - - inRecent = connStats.net.rx; - outRecent = connStats.net.tx; -} - -function onStatsLog(statsLog) { - for (const revStats of [...statsLog].reverse()) { - onStats(revStats); - } -} -</script> - -<style lang="scss" scoped> -.oxxrhrto { - display: flex; - - > svg { - display: block; - padding: 10px; - width: 50%; - - &:first-child { - padding-right: 5px; - } - - &:last-child { - padding-left: 5px; - } - - > text { - font-size: 4.5px; - fill: currentColor; - - > tspan { - opacity: 0.5; - } - } - } -} -</style> diff --git a/packages/client/src/widgets/server-metric/pie.vue b/packages/client/src/widgets/server-metric/pie.vue deleted file mode 100644 index 868dbc0484..0000000000 --- a/packages/client/src/widgets/server-metric/pie.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none"> - <circle - :r="r" - cx="50%" cy="50%" - fill="none" - stroke-width="0.1" - stroke="rgba(0, 0, 0, 0.05)" - /> - <circle - :r="r" - cx="50%" cy="50%" - :stroke-dasharray="Math.PI * (r * 2)" - :stroke-dashoffset="strokeDashoffset" - fill="none" - stroke-width="0.1" - :stroke="color" - /> - <text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text> -</svg> -</template> - -<script lang="ts" setup> -import { } from 'vue'; - -const props = defineProps<{ - value: number; -}>(); - -const r = 0.45; - -const color = $computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`); -const strokeDashoffset = $computed(() => (1 - props.value) * (Math.PI * (r * 2))); -</script> - -<style lang="scss" scoped> -.hsalcinq { - display: block; - height: 100%; - - > circle { - transform-origin: center; - transform: rotate(-90deg); - transition: stroke-dashoffset 0.5s ease; - } - - > text { - font-size: 0.15px; - fill: currentColor; - } -} -</style> diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue deleted file mode 100644 index e317b8ab94..0000000000 --- a/packages/client/src/widgets/slideshow.vue +++ /dev/null @@ -1,159 +0,0 @@ -<template> -<div class="kvausudm _panel mkw-slideshow" :style="{ height: widgetProps.height + 'px' }"> - <div @click="choose"> - <p v-if="widgetProps.folderId == null"> - {{ i18n.ts.folder }} - </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" setup> -import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import { useInterval } from '@/scripts/use-interval'; -import { i18n } from '@/i18n'; - -const name = 'slideshow'; - -const widgetPropsDef = { - height: { - type: 'number' as const, - default: 300, - }, - folderId: { - type: 'string' as const, - default: null, - hidden: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure, save } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -const images = ref([]); -const fetching = ref(true); -const slideA = ref<HTMLElement>(); -const slideB = ref<HTMLElement>(); - -const change = () => { - if (images.value.length === 0) return; - - const index = Math.floor(Math.random() * images.value.length); - const img = `url(${ images.value[index].url })`; - - slideB.value.style.backgroundImage = img; - - slideB.value.classList.add('anime'); - window.setTimeout(() => { - // 既にこのウィジェットがunmountされていたら要素がない - if (slideA.value == null) return; - - slideA.value.style.backgroundImage = img; - - slideB.value.classList.remove('anime'); - }, 1000); -}; - -const fetch = () => { - fetching.value = true; - - os.api('drive/files', { - folderId: widgetProps.folderId, - type: 'image/*', - limit: 100, - }).then(res => { - images.value = res; - fetching.value = false; - slideA.value.style.backgroundImage = ''; - slideB.value.style.backgroundImage = ''; - change(); - }); -}; - -const choose = () => { - os.selectDriveFolder(false).then(folder => { - if (folder == null) { - return; - } - widgetProps.folderId = folder.id; - save(); - fetch(); - }); -}; - -useInterval(change, 10000, { - immediate: false, - afterMounted: true, -}); - -onMounted(() => { - if (widgetProps.folderId != null) { - fetch(); - } -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.kvausudm { - position: relative; - - > div { - width: 100%; - height: 100%; - cursor: pointer; - - > p { - display: block; - margin: 1em; - text-align: center; - color: #888; - } - - > * { - pointer-events: none; - } - - > .slide { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center; - - &.b { - opacity: 0; - } - - &.anime { - transition: opacity 1s; - opacity: 1; - } - } - } -} -</style> diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue deleted file mode 100644 index e48444d33f..0000000000 --- a/packages/client/src/widgets/timeline.vue +++ /dev/null @@ -1,129 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" class="mkw-timeline"> - <template #header> - <button class="_button" @click="choose"> - <i v-if="widgetProps.src === 'home'" class="ti ti-home"></i> - <i v-else-if="widgetProps.src === 'local'" class="ti ti-messages"></i> - <i v-else-if="widgetProps.src === 'social'" class="ti ti-share"></i> - <i v-else-if="widgetProps.src === 'global'" class="ti ti-world"></i> - <i v-else-if="widgetProps.src === 'list'" class="ti ti-list"></i> - <i v-else-if="widgetProps.src === 'antenna'" class="ti ti-antenna"></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 ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i> - </button> - </template> - - <div> - <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" 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/MkContainer.vue'; -import XTimeline from '@/components/MkTimeline.vue'; -import { $i } from '@/account'; -import { i18n } from '@/i18n'; - -const name = 'timeline'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, - 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<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure, save } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -const menuOpened = ref(false); - -const setSrc = (src) => { - widgetProps.src = src; - save(); -}; - -const choose = async (ev) => { - menuOpened.value = true; - const [antennas, lists] = await Promise.all([ - os.api('antennas/list'), - os.api('users/lists/list'), - ]); - const antennaItems = antennas.map(antenna => ({ - text: antenna.name, - icon: 'ti ti-antenna', - action: () => { - widgetProps.antenna = antenna; - setSrc('antenna'); - }, - })); - const listItems = lists.map(list => ({ - text: list.name, - icon: 'ti ti-list', - action: () => { - widgetProps.list = list; - setSrc('list'); - }, - })); - os.popupMenu([{ - text: i18n.ts._timelines.home, - icon: 'ti ti-home', - action: () => { setSrc('home'); }, - }, { - text: i18n.ts._timelines.local, - icon: 'ti ti-messages', - action: () => { setSrc('local'); }, - }, { - text: i18n.ts._timelines.social, - icon: 'ti ti-share', - action: () => { setSrc('social'); }, - }, { - text: i18n.ts._timelines.global, - icon: 'ti ti-world', - action: () => { setSrc('global'); }, - }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { - menuOpened.value = false; - }); -}; - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue deleted file mode 100644 index 02eec0431e..0000000000 --- a/packages/client/src/widgets/trends.vue +++ /dev/null @@ -1,120 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-trends"> - <template #header><i class="ti ti-hash"></i>{{ i18n.ts._widgets.trends }}</template> - - <div class="wbrkwala"> - <MkLoading v-if="fetching"/> - <transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="tags"> - <div v-for="stat in stats" :key="stat.tag"> - <div class="tag"> - <MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA> - <p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> - </div> - <MkMiniChart class="chart" :src="stat.chart"/> - </div> - </transition-group> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import MkContainer from '@/components/MkContainer.vue'; -import MkMiniChart from '@/components/MkMiniChart.vue'; -import * as os from '@/os'; -import { useInterval } from '@/scripts/use-interval'; -import { i18n } from '@/i18n'; - -const name = 'hashtags'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: '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(res => { - stats.value = res; - fetching.value = false; - }); -}; - -useInterval(fetch, 1000 * 60, { - immediate: true, - afterMounted: true, -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.wbrkwala { - height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px; - overflow: hidden; - - > .tags { - .chart-move { - transition: transform 1s ease; - } - - > div { - display: flex; - align-items: center; - padding: 14px 16px; - border-bottom: solid 0.5px var(--divider); - - > .tag { - flex: 1; - overflow: hidden; - font-size: 0.9em; - color: var(--fg); - - > .a { - display: block; - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 18px; - } - - > p { - margin: 0; - font-size: 75%; - opacity: 0.7; - line-height: 16px; - } - } - - > .chart { - height: 30px; - } - } - } -} -</style> diff --git a/packages/client/src/widgets/unix-clock.vue b/packages/client/src/widgets/unix-clock.vue deleted file mode 100644 index cf85ac782c..0000000000 --- a/packages/client/src/widgets/unix-clock.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> -<div class="mkw-unixClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }"> - <div v-if="widgetProps.showLabel" class="label">UNIX Epoch</div> - <div class="time"> - <span v-text="ss"></span> - <span v-if="widgetProps.showMs" class="colon" :class="{ showColon }">:</span> - <span v-if="widgetProps.showMs" v-text="ms"></span> - </div> - <div v-if="widgetProps.showLabel" class="label">UTC</div> -</div> -</template> - -<script lang="ts" setup> -import { onUnmounted, ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; - -const name = 'unixClock'; - -const widgetPropsDef = { - transparent: { - type: 'boolean' as const, - default: false, - }, - fontSize: { - type: 'number' as const, - default: 1.5, - step: 0.1, - }, - showMs: { - type: 'boolean' as const, - default: true, - }, - showLabel: { - type: 'boolean' as const, - default: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -let intervalId; -const ss = ref(''); -const ms = ref(''); -const showColon = ref(false); -let prevSec: string | null = null; - -watch(showColon, (v) => { - if (v) { - window.setTimeout(() => { - showColon.value = false; - }, 30); - } -}); - -const tick = () => { - const now = new Date(); - ss.value = Math.floor(now.getTime() / 1000).toString(); - ms.value = Math.floor(now.getTime() % 1000 / 10).toString().padStart(2, '0'); - if (ss.value !== prevSec) showColon.value = true; - prevSec = ss.value; -}; - -tick(); - -watch(() => widgetProps.showMs, () => { - if (intervalId) window.clearInterval(intervalId); - intervalId = window.setInterval(tick, widgetProps.showMs ? 10 : 1000); -}, { immediate: true }); - -onUnmounted(() => { - window.clearInterval(intervalId); -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" scoped> -.mkw-unixClock { - padding: 16px 0; - text-align: center; - - > .label { - font-size: 65%; - opacity: 0.7; - } - - > .time { - > .colon { - opacity: 0; - transition: opacity 1s ease; - - &.showColon { - opacity: 1; - transition: opacity 0s; - } - } - } -} -</style> diff --git a/packages/client/src/widgets/user-list.vue b/packages/client/src/widgets/user-list.vue deleted file mode 100644 index 9ffbf0d8e3..0000000000 --- a/packages/client/src/widgets/user-list.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-userList"> - <template #header><i class="ti ti-users"></i>{{ list ? list.name : i18n.ts._widgets.userList }}</template> - <template #func><button class="_button" @click="configure()"><i class="ti ti-settings"></i></button></template> - - <div :class="$style.root"> - <div v-if="widgetProps.listId == null" class="init"> - <MkButton primary @click="chooseList">{{ i18n.ts._widgets._userList.chooseList }}</MkButton> - </div> - <MkLoading v-else-if="fetching"/> - <div v-else class="users"> - <MkA v-for="user in users" :key="user.id" class="user"> - <MkAvatar :user="user" class="avatar" :show-indicator="true"/> - </MkA> - </div> - </div> -</MkContainer> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; -import { GetFormResultType } from '@/scripts/form'; -import MkContainer from '@/components/MkContainer.vue'; -import * as os from '@/os'; -import { useInterval } from '@/scripts/use-interval'; -import { i18n } from '@/i18n'; -import MkButton from '@/components/MkButton.vue'; - -const name = 'userList'; - -const widgetPropsDef = { - showHeader: { - type: 'boolean' as const, - default: true, - }, - listId: { - type: 'string' as const, - default: null, - hidden: true, - }, -}; - -type WidgetProps = GetFormResultType<typeof widgetPropsDef>; - -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - -const { widgetProps, configure, save } = useWidgetPropsManager(name, - widgetPropsDef, - props, - emit, -); - -let list = $ref(); -let users = $ref([]); -let fetching = $ref(true); - -async function chooseList() { - const lists = await os.api('users/lists/list'); - const { canceled, result: list } = await os.select({ - title: i18n.ts.selectList, - items: lists.map(x => ({ - value: x, text: x.name, - })), - default: widgetProps.listId, - }); - if (canceled) return; - - widgetProps.listId = list.id; - save(); - fetch(); -} - -const fetch = () => { - if (widgetProps.listId == null) { - fetching = false; - return; - } - - os.api('users/lists/show', { - listId: widgetProps.listId, - }).then(_list => { - list = _list; - os.api('users/show', { - userIds: list.userIds, - }).then(_users => { - users = _users; - fetching = false; - }); - }); -}; - -useInterval(fetch, 1000 * 60, { - immediate: true, - afterMounted: true, -}); - -defineExpose<WidgetComponentExpose>({ - name, - configure, - id: props.widget ? props.widget.id : null, -}); -</script> - -<style lang="scss" module> -.root { - &:global { - > .init { - padding: 16px; - } - - > .users { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(30px, 40px)); - grid-gap: 12px; - place-content: center; - padding: 16px; - - > .user { - width: 100%; - height: 100%; - aspect-ratio: 1; - - > .avatar { - width: 100%; - height: 100%; - } - } - } - } -} -</style> diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts deleted file mode 100644 index 8bd56a5966..0000000000 --- a/packages/client/src/widgets/widget.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { reactive, watch } from 'vue'; -import { throttle } from 'throttle-debounce'; -import { Form, GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import { deepClone } from '@/scripts/clone'; - -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>> = { - (ev: '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 ? deepClone(props.widget.data) : {}); - - const mergeProps = () => { - for (const prop of Object.keys(propsDef)) { - if (typeof widgetProps[prop] === 'undefined') { - widgetProps[prop] = propsDef[prop].default; - } - } - }; - watch(widgetProps, () => { - mergeProps(); - }, { deep: true, immediate: true }); - - const save = throttle(3000, () => { - emit('updateProps', widgetProps); - }); - - const configure = async () => { - const form = deepClone(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, - }; -}; |