diff options
Diffstat (limited to 'src/client/widgets')
29 files changed, 0 insertions, 2934 deletions
diff --git a/src/client/widgets/activity.calendar.vue b/src/client/widgets/activity.calendar.vue deleted file mode 100644 index ff767190f6..0000000000 --- a/src/client/widgets/activity.calendar.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<svg viewBox="0 0 21 7"> - <rect v-for="record in data" 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 data" 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="data[0].x" :y="data[0].date.weekday" - rx="1" ry="1" - fill="none" - stroke-width="0.1" - stroke="#f73520"/> -</svg> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; - -export default defineComponent({ - props: ['data'], - created() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); - - let x = 20; - this.data.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/src/client/widgets/activity.chart.vue b/src/client/widgets/activity.chart.vue deleted file mode 100644 index ee5bc25113..0000000000 --- a/src/client/widgets/activity.chart.vue +++ /dev/null @@ -1,107 +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"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; - -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); -} - -export default defineComponent({ - props: ['data'], - data() { - return { - viewBoxX: 147, - viewBoxY: 60, - zoom: 1, - pos: 0, - pointsNote: null, - pointsReply: null, - pointsRenote: null, - pointsTotal: null - }; - }, - created() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } - - this.render(); - }, - methods: { - render() { - const peak = Math.max.apply(null, this.data.map(d => d.total)); - if (peak != 0) { - const data = this.data.slice().reverse(); - this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '); - this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '); - this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '); - this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); - } - }, - onMousedown(e) { - const clickX = e.clientX; - const clickY = e.clientY; - const baseZoom = this.zoom; - const basePos = this.pos; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - clickX; - let moveTop = me.clientY - clickY; - - this.zoom = baseZoom + (-moveTop / 20); - this.pos = basePos + moveLeft; - if (this.zoom < 1) this.zoom = 1; - if (this.pos > 0) this.pos = 0; - if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX); - - this.render(); - }); - } - } -}); -</script> - -<style lang="scss" scoped> -svg { - display: block; - padding: 16px; - width: 100%; - box-sizing: border-box; - cursor: all-scroll; -} -</style> diff --git a/src/client/widgets/activity.vue b/src/client/widgets/activity.vue deleted file mode 100644 index cc8d4debd0..0000000000 --- a/src/client/widgets/activity.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader" :naked="props.transparent"> - <template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template> - <template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template> - - <div> - <MkLoading v-if="fetching"/> - <template v-else> - <XCalendar v-show="props.view === 0" :data="[].concat(activity)"/> - <XChart v-show="props.view === 1" :data="[].concat(activity)"/> - </template> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import define from './define'; -import XCalendar from './activity.calendar.vue'; -import XChart from './activity.chart.vue'; -import * as os from '@client/os'; - -const widget = define({ - name: 'activity', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - transparent: { - type: 'boolean', - default: false, - }, - view: { - type: 'number', - default: 0, - hidden: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, - XCalendar, - XChart, - }, - data() { - return { - fetching: true, - activity: null, - }; - }, - mounted() { - os.api('charts/user/notes', { - userId: this.$i.id, - span: 'day', - limit: 7 * 21 - }).then(activity => { - this.activity = activity.diffs.normal.map((_, i) => ({ - total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i], - notes: activity.diffs.normal[i], - replies: activity.diffs.reply[i], - renotes: activity.diffs.renote[i] - })); - this.fetching = false; - }); - }, - methods: { - toggleView() { - if (this.props.view === 1) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - } - } -}); -</script> diff --git a/src/client/widgets/aichan.vue b/src/client/widgets/aichan.vue deleted file mode 100644 index 06c49090a1..0000000000 --- a/src/client/widgets/aichan.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> -<MkContainer :naked="props.transparent" :show-header="false"> - <iframe class="dedjhjmo" ref="live2d" @click="touched" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100"></iframe> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import define from './define'; -import MkContainer from '@client/components/ui/container.vue'; -import * as os from '@client/os'; - -const widget = define({ - name: 'ai', - props: () => ({ - transparent: { - type: 'boolean', - default: false, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, - }, - data() { - return { - }; - }, - mounted() { - window.addEventListener('mousemove', ev => { - const iframeRect = this.$refs.live2d.getBoundingClientRect(); - this.$refs.live2d.contentWindow.postMessage({ - type: 'moveCursor', - body: { - x: ev.clientX - iframeRect.left, - y: ev.clientY - iframeRect.top, - } - }, '*'); - }, { passive: true }); - }, - methods: { - touched() { - //if (this.live2d) this.live2d.changeExpression('gurugurume'); - } - } -}); -</script> - -<style lang="scss" scoped> -.dedjhjmo { - width: 100%; - height: 350px; - border: none; - pointer-events: none; -} -</style> diff --git a/src/client/widgets/aiscript.vue b/src/client/widgets/aiscript.vue deleted file mode 100644 index aaf0a0372e..0000000000 --- a/src/client/widgets/aiscript.vue +++ /dev/null @@ -1,163 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader"> - <template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template> - - <div class="uylguesu _monospace"> - <textarea v-model="props.script" placeholder="(1 + 1)"></textarea> - <button @click="run" class="_buttonPrimary">RUN</button> - <div class="logs"> - <div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> - </div> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import define from './define'; -import * as os from '@client/os'; -import { AiScript, parse, utils } from '@syuilo/aiscript'; -import { createAiScriptEnv } from '@client/scripts/aiscript/api'; - -const widget = define({ - name: 'aiscript', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - script: { - type: 'string', - multiline: true, - default: '(1 + 1)', - hidden: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer - }, - - data() { - return { - logs: [], - }; - }, - - methods: { - async run() { - this.logs = []; - const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'widget', - token: this.$i?.token, - }), { - in: (q) => { - return new Promise(ok => { - os.dialog({ - title: q, - input: {} - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - this.logs.push({ - id: Math.random(), - text: value.type === 'str' ? value.value : utils.valToString(value), - print: true - }); - }, - log: (type, params) => { - switch (type) { - case 'end': this.logs.push({ - id: Math.random(), - text: utils.valToString(params.val, true), - print: false - }); break; - default: break; - } - } - }); - - let ast; - try { - ast = parse(this.props.script); - } catch (e) { - os.dialog({ - type: 'error', - text: 'Syntax error :(' - }); - return; - } - try { - await aiscript.exec(ast); - } catch (e) { - os.dialog({ - type: 'error', - text: e - }); - } - }, - } -}); -</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/src/client/widgets/button.vue b/src/client/widgets/button.vue deleted file mode 100644 index af6718c507..0000000000 --- a/src/client/widgets/button.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> -<div class="mkw-button"> - <MkButton :primary="props.colored" full @click="run"> - {{ props.label }} - </MkButton> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import define from './define'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import { AiScript, parse, utils } from '@syuilo/aiscript'; -import { createAiScriptEnv } from '@client/scripts/aiscript/api'; - -const widget = define({ - name: 'button', - props: () => ({ - label: { - type: 'string', - default: 'BUTTON', - }, - colored: { - type: 'boolean', - default: true, - }, - script: { - type: 'string', - multiline: true, - default: 'Mk:dialog("hello" "world")', - }, - }) -}); - -export default defineComponent({ - components: { - MkButton - }, - extends: widget, - data() { - return { - }; - }, - methods: { - async run() { - const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'widget', - token: this.$i?.token, - }), { - in: (q) => { - return new Promise(ok => { - os.dialog({ - title: q, - input: {} - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - // nop - }, - log: (type, params) => { - // nop - } - }); - - let ast; - try { - ast = parse(this.props.script); - } catch (e) { - os.dialog({ - type: 'error', - text: 'Syntax error :(' - }); - return; - } - try { - await aiscript.exec(ast); - } catch (e) { - os.dialog({ - type: 'error', - text: e - }); - } - }, - } -}); -</script> - -<style lang="scss" scoped> -.mkw-button { -} -</style> diff --git a/src/client/widgets/calendar.vue b/src/client/widgets/calendar.vue deleted file mode 100644 index fe39145f0d..0000000000 --- a/src/client/widgets/calendar.vue +++ /dev/null @@ -1,204 +0,0 @@ -<template> -<div class="mkw-calendar" :class="{ _panel: !props.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 class="day">{{ $t('dayX', { day }) }}</p> - <p class="week-day">{{ weekDay }}</p> - </div> - <div class="info"> - <div> - <p>{{ $ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${dayP}%` }"></div> - </div> - </div> - <div> - <p>{{ $ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${monthP}%` }"></div> - </div> - </div> - <div> - <p>{{ $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"> -import { defineComponent } from 'vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'calendar', - props: () => ({ - transparent: { - type: 'boolean', - default: false, - }, - }) -}); - -export default defineComponent({ - extends: widget, - data() { - return { - now: new Date(), - year: null, - month: null, - day: null, - weekDay: null, - yearP: null, - dayP: null, - monthP: null, - isHoliday: null, - clock: null - }; - }, - created() { - this.tick(); - this.clock = setInterval(this.tick, 1000); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - tick() { - const now = new Date(); - const nd = now.getDate(); - const nm = now.getMonth(); - const ny = now.getFullYear(); - - this.year = ny; - this.month = nm + 1; - this.day = nd; - this.weekDay = [ - this.$ts._weekday.sunday, - this.$ts._weekday.monday, - this.$ts._weekday.tuesday, - this.$ts._weekday.wednesday, - this.$ts._weekday.thursday, - this.$ts._weekday.friday, - this.$ts._weekday.saturday - ][now.getDay()]; - - const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime(); - const dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/; - const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime(); - const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime(); - const yearNumer = now.getTime() - new Date(ny, 0, 1).getTime(); - const yearDenom = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime(); - - this.dayP = dayNumer / dayDenom * 100; - this.monthP = monthNumer / monthDenom * 100; - this.yearP = yearNumer / yearDenom * 100; - - this.isHoliday = now.getDay() === 0 || now.getDay() === 6; - } - } -}); -</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; - } - } - - > p { - margin: 0; - line-height: 18px; - font-size: 0.9em; - - > span { - 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/src/client/widgets/clock.vue b/src/client/widgets/clock.vue deleted file mode 100644 index d960c3809a..0000000000 --- a/src/client/widgets/clock.vue +++ /dev/null @@ -1,55 +0,0 @@ -<template> -<MkContainer :naked="props.transparent" :show-header="false"> - <div class="vubelbmv"> - <MkAnalogClock class="clock" :thickness="props.thickness"/> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import define from './define'; -import MkContainer from '@client/components/ui/container.vue'; -import MkAnalogClock from '@client/components/analog-clock.vue'; -import * as os from '@client/os'; - -const widget = define({ - name: 'clock', - props: () => ({ - transparent: { - type: 'boolean', - default: false, - }, - thickness: { - type: 'radio', - default: 0.1, - options: [{ - value: 0.1, label: 'thin' - }, { - value: 0.2, label: 'medium' - }, { - value: 0.3, label: 'thick' - }] - } - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, - MkAnalogClock - }, -}); -</script> - -<style lang="scss" scoped> -.vubelbmv { - padding: 8px; - - > .clock { - height: 150px; - margin: auto; - } -} -</style> diff --git a/src/client/widgets/define.ts b/src/client/widgets/define.ts deleted file mode 100644 index 22b7fb30a1..0000000000 --- a/src/client/widgets/define.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { defineComponent } from 'vue'; -import { throttle } from 'throttle-debounce'; -import { Form } from '@client/scripts/form'; -import * as os from '@client/os'; - -export default function <T extends Form>(data: { - name: string; - props?: () => T; -}) { - return defineComponent({ - props: { - widget: { - type: Object, - required: false - }, - settingCallback: { - required: false - } - }, - - emits: ['updateProps'], - - data() { - return { - props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {}, - save: throttle(3000, () => { - this.$emit('updateProps', this.props); - }), - }; - }, - - computed: { - id(): string { - return this.widget ? this.widget.id : null; - }, - }, - - created() { - this.mergeProps(); - - this.$watch('props', () => { - this.mergeProps(); - }, { deep: true }); - - if (this.settingCallback) this.settingCallback(this.setting); - }, - - methods: { - mergeProps() { - if (data.props) { - const defaultProps = data.props(); - for (const prop of Object.keys(defaultProps)) { - if (this.props.hasOwnProperty(prop)) continue; - this.props[prop] = defaultProps[prop].default; - } - } - }, - - async setting() { - const form = data.props(); - for (const item of Object.keys(form)) { - form[item].default = this.props[item]; - } - const { canceled, result } = await os.form(data.name, form); - if (canceled) return; - - for (const key of Object.keys(result)) { - this.props[key] = result[key]; - } - - this.save(); - }, - } - }); -} diff --git a/src/client/widgets/digital-clock.vue b/src/client/widgets/digital-clock.vue deleted file mode 100644 index 2202c9ed4b..0000000000 --- a/src/client/widgets/digital-clock.vue +++ /dev/null @@ -1,79 +0,0 @@ -<template> -<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }"> - <span> - <span v-text="hh"></span> - <span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> - <span v-text="mm"></span> - <span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> - <span v-text="ss"></span> - <span :style="{ visibility: showColon ? 'visible' : 'hidden' }" v-if="props.showMs">:</span> - <span v-text="ms" v-if="props.showMs"></span> - </span> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'digitalClock', - props: () => ({ - transparent: { - type: 'boolean', - default: false, - }, - fontSize: { - type: 'number', - default: 1.5, - step: 0.1, - }, - showMs: { - type: 'boolean', - default: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - data() { - return { - clock: null, - hh: null, - mm: null, - ss: null, - ms: null, - showColon: true, - }; - }, - created() { - this.tick(); - this.$watch(() => this.props.showMs, () => { - if (this.clock) clearInterval(this.clock); - this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000); - }, { immediate: true }); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - tick() { - const now = new Date(); - this.hh = now.getHours().toString().padStart(2, '0'); - this.mm = now.getMinutes().toString().padStart(2, '0'); - this.ss = now.getSeconds().toString().padStart(2, '0'); - this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0'); - this.showColon = now.getSeconds() % 2 === 0; - } - } -}); -</script> - -<style lang="scss" scoped> -.mkw-digitalClock { - padding: 16px 0; - text-align: center; -} -</style> diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue deleted file mode 100644 index 8ab7f594a2..0000000000 --- a/src/client/widgets/federation.vue +++ /dev/null @@ -1,145 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable"> - <template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template> - - <div class="wbrkwalb"> - <MkLoading v-if="fetching"/> - <transition-group tag="div" name="chart" class="instances" v-else> - <div v-for="(instance, i) in instances" :key="instance.id" class="instance"> - <img v-if="instance.iconUrl" :src="instance.iconUrl" 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"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import define from './define'; -import MkMiniChart from '@client/components/mini-chart.vue'; -import * as os from '@client/os'; - -const widget = define({ - name: 'federation', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, MkMiniChart - }, - props: { - foldable: { - type: Boolean, - required: false, - default: false - }, - scrollable: { - type: Boolean, - required: false, - default: false - }, - }, - data() { - return { - instances: [], - charts: [], - fetching: true, - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - async fetch() { - const instances = await os.api('federation/instances', { - sort: '+lastCommunicatedAt', - limit: 5 - }); - const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); - this.instances = instances; - this.charts = charts; - this.fetching = false; - } - } -}); -</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/src/client/widgets/index.ts b/src/client/widgets/index.ts deleted file mode 100644 index 51a82af080..0000000000 --- a/src/client/widgets/index.ts +++ /dev/null @@ -1,45 +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('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('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('MkwButton', defineAsyncComponent(() => import('./button.vue'))); - app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue'))); - app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue'))); -} - -export const widgets = [ - 'memo', - 'notifications', - 'timeline', - 'calendar', - 'rss', - 'trends', - 'clock', - 'activity', - 'photos', - 'digitalClock', - 'federation', - 'postForm', - 'slideshow', - 'serverMetric', - 'onlineUsers', - 'jobQueue', - 'button', - 'aiscript', - 'aichan', -]; diff --git a/src/client/widgets/job-queue.vue b/src/client/widgets/job-queue.vue deleted file mode 100644 index 327d8ede6d..0000000000 --- a/src/client/widgets/job-queue.vue +++ /dev/null @@ -1,183 +0,0 @@ -<template> -<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }"> - <div class="inbox"> - <div class="label">Inbox queue<i v-if="inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div> - <div class="values"> - <div> - <div>Process</div> - <div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div> - </div> - <div> - <div>Active</div> - <div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div> - </div> - <div> - <div>Delayed</div> - <div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div> - </div> - <div> - <div>Waiting</div> - <div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div> - </div> - </div> - </div> - <div class="deliver"> - <div class="label">Deliver queue<i v-if="deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div> - <div class="values"> - <div> - <div>Process</div> - <div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div> - </div> - <div> - <div>Active</div> - <div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div> - </div> - <div> - <div>Delayed</div> - <div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div> - </div> - <div> - <div>Waiting</div> - <div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import define from './define'; -import * as os from '@client/os'; -import number from '@client/filters/number'; -import * as sound from '@client/scripts/sound'; - -const widget = define({ - name: 'jobQueue', - props: () => ({ - transparent: { - type: 'boolean', - default: false, - }, - sound: { - type: 'boolean', - default: false, - }, - }) -}); - -export default defineComponent({ - extends: widget, - data() { - return { - connection: markRaw(os.stream.useChannel('queueStats')), - inbox: { - activeSincePrevTick: 0, - active: 0, - waiting: 0, - delayed: 0, - }, - deliver: { - activeSincePrevTick: 0, - active: 0, - waiting: 0, - delayed: 0, - }, - prev: {}, - sound: sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1) - }; - }, - created() { - for (const domain of ['inbox', 'deliver']) { - this.prev[domain] = JSON.parse(JSON.stringify(this[domain])); - } - - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 1 - }); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - this.connection.dispose(); - }, - methods: { - onStats(stats) { - for (const domain of ['inbox', 'deliver']) { - this.prev[domain] = JSON.parse(JSON.stringify(this[domain])); - this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick; - this[domain].active = stats[domain].active; - this[domain].waiting = stats[domain].waiting; - this[domain].delayed = stats[domain].delayed; - - if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) { - this.sound.play(); - } - } - }, - - onStatsLog(statsLog) { - for (const stats of [...statsLog].reverse()) { - this.onStats(stats); - } - }, - - number - } -}); -</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/src/client/widgets/memo.vue b/src/client/widgets/memo.vue deleted file mode 100644 index 3f11e6409e..0000000000 --- a/src/client/widgets/memo.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader"> - <template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template> - - <div class="otgbylcu"> - <textarea v-model="text" :placeholder="$ts.placeholder" @input="onChange"></textarea> - <button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $ts.save }}</button> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'memo', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer - }, - - data() { - return { - text: null, - changed: false, - timeoutId: null, - }; - }, - - created() { - this.text = this.$store.state.memo; - - this.$watch(() => this.$store.reactiveState.memo, text => { - this.text = text; - }); - }, - - methods: { - onChange() { - this.changed = true; - clearTimeout(this.timeoutId); - this.timeoutId = setTimeout(this.saveMemo, 1000); - }, - - saveMemo() { - this.$store.set('memo', this.text); - this.changed = false; - } - } -}); -</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/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue deleted file mode 100644 index 5e2648f5b9..0000000000 --- a/src/client/widgets/notifications.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true"> - <template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template> - <template #func><button @click="configure()" class="_button"><i class="fas fa-cog"></i></button></template> - - <div> - <XNotifications :include-types="props.includingTypes"/> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import XNotifications from '@client/components/notifications.vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'notifications', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - height: { - type: 'number', - default: 300, - }, - includingTypes: { - type: 'array', - hidden: true, - default: null, - }, - }) -}); - -export default defineComponent({ - extends: widget, - - components: { - MkContainer, - XNotifications, - }, - - data() { - return { - }; - }, - - methods: { - configure() { - os.popup(import('@client/components/notification-setting-window.vue'), { - includingTypes: this.props.includingTypes, - }, { - done: async (res) => { - const { includingTypes } = res; - this.props.includingTypes = includingTypes; - this.save(); - } - }, 'closed'); - } - } -}); -</script> diff --git a/src/client/widgets/online-users.vue b/src/client/widgets/online-users.vue deleted file mode 100644 index 37060fca43..0000000000 --- a/src/client/widgets/online-users.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }"> - <I18n v-if="onlineUsersCount" :src="$ts.onlineUsersCount" text-tag="span" class="text"> - <template #n><b>{{ onlineUsersCount }}</b></template> - </I18n> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'onlineUsers', - props: () => ({ - transparent: { - type: 'boolean', - default: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - data() { - return { - onlineUsersCount: null, - clock: null, - }; - }, - created() { - this.tick(); - this.clock = setInterval(this.tick, 1000 * 15); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - tick() { - os.api('get-online-users-count').then(res => { - this.onlineUsersCount = res.count; - }); - } - } -}); -</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/src/client/widgets/photos.vue b/src/client/widgets/photos.vue deleted file mode 100644 index 25365d6b87..0000000000 --- a/src/client/widgets/photos.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null"> - <template #header><i class="fas fa-camera"></i>{{ $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"> -import { defineComponent, markRaw } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import define from './define'; -import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; -import * as os from '@client/os'; - -const widget = define({ - name: 'photos', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - transparent: { - type: 'boolean', - default: false, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, - }, - data() { - return { - images: [], - fetching: true, - connection: null, - }; - }, - mounted() { - this.connection = markRaw(os.stream.useChannel('main')); - - this.connection.on('driveFileCreated', this.onDriveFileCreated); - - os.api('drive/stream', { - type: 'image/*', - limit: 9 - }).then(images => { - this.images = images; - this.fetching = false; - }); - }, - beforeUnmount() { - this.connection.dispose(); - }, - methods: { - onDriveFileCreated(file) { - if (/^image\/.+$/.test(file.type)) { - this.images.unshift(file); - if (this.images.length > 9) this.images.pop(); - } - }, - - thumbnail(image: any): string { - return this.$store.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - } -}); -</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/src/client/widgets/post-form.vue b/src/client/widgets/post-form.vue deleted file mode 100644 index 1f260c20d9..0000000000 --- a/src/client/widgets/post-form.vue +++ /dev/null @@ -1,23 +0,0 @@ -<template> -<XPostForm class="_panel" :fixed="true" :autofocus="false"/> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XPostForm from '@client/components/post-form.vue'; -import define from './define'; - -const widget = define({ - name: 'postForm', - props: () => ({ - }) -}); - -export default defineComponent({ - extends: widget, - - components: { - XPostForm, - }, -}); -</script> diff --git a/src/client/widgets/rss.vue b/src/client/widgets/rss.vue deleted file mode 100644 index 6d19a86dff..0000000000 --- a/src/client/widgets/rss.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader"> - <template #header><i class="fas fa-rss-square"></i>RSS</template> - <template #func><button class="_button" @click="setting"><i class="fas fa-cog"></i></button></template> - - <div class="ekmkgxbj"> - <MkLoading v-if="fetching"/> - <div class="feed" v-else> - <a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> - </div> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'rss', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - url: { - type: 'string', - default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer - }, - data() { - return { - items: [], - fetching: true, - clock: null, - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 60000); - this.$watch(() => this.props.url, this.fetch); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - fetch() { - fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, { - }).then(res => { - res.json().then(feed => { - this.items = feed.items; - this.fetching = false; - }); - }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.ekmkgxbj { - > .feed { - padding: 0; - font-size: 0.9em; - - > a { - 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/src/client/widgets/server-metric/cpu-mem.vue b/src/client/widgets/server-metric/cpu-mem.vue deleted file mode 100644 index ad9e6a8b0f..0000000000 --- a/src/client/widgets/server-metric/cpu-mem.vue +++ /dev/null @@ -1,174 +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"> -import { defineComponent } from 'vue'; -import { v4 as uuid } from 'uuid'; - -export default defineComponent({ - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - viewBoxX: 50, - viewBoxY: 30, - stats: [], - cpuGradientId: uuid(), - cpuMaskId: uuid(), - memGradientId: uuid(), - memMaskId: uuid(), - cpuPolylinePoints: '', - memPolylinePoints: '', - cpuPolygonPoints: '', - memPolygonPoints: '', - cpuHeadX: null, - cpuHeadY: null, - memHeadX: null, - memHeadY: null, - cpuP: '', - memP: '' - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8) - }); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - }, - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - - const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu) * this.viewBoxY]); - const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.active / this.meta.mem.total)) * this.viewBoxY]); - this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - - this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0]; - this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1]; - this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0]; - this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1]; - - this.cpuP = (stats.cpu * 100).toFixed(0); - this.memP = (stats.mem.active / this.meta.mem.total * 100).toFixed(0); - }, - onStatsLog(statsLog) { - for (const stats of [...statsLog].reverse()) { - this.onStats(stats); - } - } - } -}); -</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: 5px; - fill: currentColor; - - > tspan { - opacity: 0.5; - } - } - } -} -</style> diff --git a/src/client/widgets/server-metric/cpu.vue b/src/client/widgets/server-metric/cpu.vue deleted file mode 100644 index 4478ee3065..0000000000 --- a/src/client/widgets/server-metric/cpu.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="vrvdvrys"> - <XPie class="pie" :value="usage"/> - <div> - <p><i class="fas fa-microchip"></i>CPU</p> - <p>{{ meta.cpu.cores }} Logical cores</p> - <p>{{ meta.cpu.model }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XPie from './pie.vue'; - -export default defineComponent({ - components: { - XPie - }, - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - usage: 0, - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.usage = stats.cpu; - } - } -}); -</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/src/client/widgets/server-metric/disk.vue b/src/client/widgets/server-metric/disk.vue deleted file mode 100644 index a3f5d0376b..0000000000 --- a/src/client/widgets/server-metric/disk.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<div class="zbwaqsat"> - <XPie class="pie" :value="usage"/> - <div> - <p><i class="fas fa-hdd"></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"> -import { defineComponent } from 'vue'; -import XPie from './pie.vue'; -import bytes from '@client/filters/bytes'; - -export default defineComponent({ - components: { - XPie - }, - props: { - meta: { - required: true, - } - }, - data() { - return { - usage: this.meta.fs.used / this.meta.fs.total, - total: this.meta.fs.total, - used: this.meta.fs.used, - available: this.meta.fs.total - this.meta.fs.used, - }; - }, - methods: { - bytes - } -}); -</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/src/client/widgets/server-metric/index.vue b/src/client/widgets/server-metric/index.vue deleted file mode 100644 index 45cd8cebf2..0000000000 --- a/src/client/widgets/server-metric/index.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader" :naked="props.transparent"> - <template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template> - <template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template> - - <div class="mkw-serverMetric" v-if="meta"> - <XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/> - <XNet v-if="props.view === 1" :connection="connection" :meta="meta"/> - <XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/> - <XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/> - <XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import define from '../define'; -import MkContainer from '@client/components/ui/container.vue'; -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 * as os from '@client/os'; - -const widget = define({ - name: 'serverMetric', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - transparent: { - type: 'boolean', - default: false, - }, - view: { - type: 'number', - default: 0, - hidden: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, - XCpuMemory, - XNet, - XCpu, - XMemory, - XDisk, - }, - data() { - return { - meta: null, - connection: null, - }; - }, - created() { - os.api('server-info', {}).then(res => { - this.meta = res; - }); - this.connection = markRaw(os.stream.useChannel('serverStats')); - }, - unmounted() { - this.connection.dispose(); - }, - methods: { - toggleView() { - if (this.props.view == 4) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - }, - } -}); -</script> diff --git a/src/client/widgets/server-metric/mem.vue b/src/client/widgets/server-metric/mem.vue deleted file mode 100644 index 92c0aa0c77..0000000000 --- a/src/client/widgets/server-metric/mem.vue +++ /dev/null @@ -1,85 +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"> -import { defineComponent } from 'vue'; -import XPie from './pie.vue'; -import bytes from '@client/filters/bytes'; - -export default defineComponent({ - components: { - XPie - }, - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - usage: 0, - total: 0, - used: 0, - free: 0, - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.usage = stats.mem.active / this.meta.mem.total; - this.total = this.meta.mem.total; - this.used = stats.mem.active; - this.free = this.meta.mem.total - stats.mem.active; - }, - bytes - } -}); -</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/src/client/widgets/server-metric/net.vue b/src/client/widgets/server-metric/net.vue deleted file mode 100644 index 569c15b58b..0000000000 --- a/src/client/widgets/server-metric/net.vue +++ /dev/null @@ -1,148 +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"> -import { defineComponent } from 'vue'; -import bytes from '@client/filters/bytes'; - -export default defineComponent({ - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - viewBoxX: 50, - viewBoxY: 30, - stats: [], - inPolylinePoints: '', - outPolylinePoints: '', - inPolygonPoints: '', - outPolygonPoints: '', - inHeadX: null, - inHeadY: null, - outHeadX: null, - outHeadY: null, - inRecent: 0, - outRecent: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8) - }); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - }, - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - - const inPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.rx))); - const outPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.tx))); - - const inPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * this.viewBoxY]); - const outPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * this.viewBoxY]); - this.inPolylinePoints = inPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.outPolylinePoints = outPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - this.inPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.inPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - this.outPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.outPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - - this.inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0]; - this.inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1]; - this.outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0]; - this.outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1]; - - this.inRecent = stats.net.rx; - this.outRecent = stats.net.tx; - }, - onStatsLog(statsLog) { - for (const stats of [...statsLog].reverse()) { - this.onStats(stats); - } - }, - bytes - } -}); -</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: 5px; - fill: currentColor; - - > tspan { - opacity: 0.5; - } - } - } -} -</style> diff --git a/src/client/widgets/server-metric/pie.vue b/src/client/widgets/server-metric/pie.vue deleted file mode 100644 index 38dcf6fcd9..0000000000 --- a/src/client/widgets/server-metric/pie.vue +++ /dev/null @@ -1,65 +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"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - value: { - type: Number, - required: true - } - }, - data() { - return { - r: 0.45 - }; - }, - computed: { - color(): string { - return `hsl(${180 - (this.value * 180)}, 80%, 70%)`; - }, - strokeDashoffset(): number { - return (1 - this.value) * (Math.PI * (this.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/src/client/widgets/slideshow.vue b/src/client/widgets/slideshow.vue deleted file mode 100644 index 2f079e0d42..0000000000 --- a/src/client/widgets/slideshow.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="kvausudm _panel"> - <div @click="choose"> - <p v-if="props.folderId == null"> - <template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template> - <template v-else>{{ $ts.folder }}</template> - </p> - <p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p> - <div ref="slideA" class="slide a"></div> - <div ref="slideB" class="slide b"></div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'slideshow', - props: () => ({ - height: { - type: 'number', - default: 300, - }, - folderId: { - type: 'string', - default: null, - hidden: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - data() { - return { - images: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.$nextTick(() => { - this.applySize(); - }); - - if (this.props.folderId != null) { - this.fetch(); - } - - this.clock = setInterval(this.change, 10000); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - applySize() { - let h; - - if (this.props.size == 1) { - h = 250; - } else { - h = 170; - } - - this.$el.style.height = `${h}px`; - }, - resize() { - if (this.props.size == 1) { - this.props.size = 0; - } else { - this.props.size++; - } - this.save(); - - this.applySize(); - }, - change() { - if (this.images.length == 0) return; - - const index = Math.floor(Math.random() * this.images.length); - const img = `url(${ this.images[index].url })`; - - (this.$refs.slideB as any).style.backgroundImage = img; - - this.$refs.slideB.classList.add('anime'); - setTimeout(() => { - // 既にこのウィジェットがunmountされていたら要素がない - if ((this.$refs.slideA as any) == null) return; - - (this.$refs.slideA as any).style.backgroundImage = img; - - this.$refs.slideB.classList.remove('anime'); - }, 1000); - }, - fetch() { - this.fetching = true; - - os.api('drive/files', { - folderId: this.props.folderId, - type: 'image/*', - limit: 100 - }).then(images => { - this.images = images; - this.fetching = false; - (this.$refs.slideA as any).style.backgroundImage = ''; - (this.$refs.slideB as any).style.backgroundImage = ''; - this.change(); - }); - }, - choose() { - os.selectDriveFolder(false).then(folder => { - if (folder == null) { - return; - } - this.props.folderId = folder.id; - this.save(); - this.fetch(); - }); - } - } -}); -</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/src/client/widgets/timeline.vue b/src/client/widgets/timeline.vue deleted file mode 100644 index bd951d8565..0000000000 --- a/src/client/widgets/timeline.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true"> - <template #header> - <button @click="choose" class="_button"> - <i v-if="props.src === 'home'" class="fas fa-home"></i> - <i v-else-if="props.src === 'local'" class="fas fa-comments"></i> - <i v-else-if="props.src === 'social'" class="fas fa-share-alt"></i> - <i v-else-if="props.src === 'global'" class="fas fa-globe"></i> - <i v-else-if="props.src === 'list'" class="fas fa-list-ul"></i> - <i v-else-if="props.src === 'antenna'" class="fas fa-satellite"></i> - <span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span> - <i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i> - </button> - </template> - - <div> - <XTimeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import XTimeline from '@client/components/timeline.vue'; -import define from './define'; -import * as os from '@client/os'; - -const widget = define({ - name: 'timeline', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - height: { - type: 'number', - default: 300, - }, - src: { - type: 'string', - default: 'home', - hidden: true, - }, - list: { - type: 'object', - default: null, - hidden: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, - XTimeline, - }, - - data() { - return { - menuOpened: false, - }; - }, - - methods: { - async choose(ev) { - this.menuOpened = true; - const [antennas, lists] = await Promise.all([ - os.api('antennas/list'), - os.api('users/lists/list') - ]); - const antennaItems = antennas.map(antenna => ({ - text: antenna.name, - icon: 'fas fa-satellite', - action: () => { - this.props.antenna = antenna; - this.setSrc('antenna'); - } - })); - const listItems = lists.map(list => ({ - text: list.name, - icon: 'fas fa-list-ul', - action: () => { - this.props.list = list; - this.setSrc('list'); - } - })); - os.popupMenu([{ - text: this.$ts._timelines.home, - icon: 'fas fa-home', - action: () => { this.setSrc('home') } - }, { - text: this.$ts._timelines.local, - icon: 'fas fa-comments', - action: () => { this.setSrc('local') } - }, { - text: this.$ts._timelines.social, - icon: 'fas fa-share-alt', - action: () => { this.setSrc('social') } - }, { - text: this.$ts._timelines.global, - icon: 'fas fa-globe', - action: () => { this.setSrc('global') } - }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => { - this.menuOpened = false; - }); - }, - - setSrc(src) { - this.props.src = src; - this.save(); - }, - } -}); -</script> diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue deleted file mode 100644 index 8511bc718f..0000000000 --- a/src/client/widgets/trends.vue +++ /dev/null @@ -1,111 +0,0 @@ -<template> -<MkContainer :show-header="props.showHeader"> - <template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template> - - <div class="wbrkwala"> - <MkLoading v-if="fetching"/> - <transition-group tag="div" name="chart" class="tags" v-else> - <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"> -import { defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import define from './define'; -import MkMiniChart from '@client/components/mini-chart.vue'; -import * as os from '@client/os'; - -const widget = define({ - name: 'hashtags', - props: () => ({ - showHeader: { - type: 'boolean', - default: true, - }, - }) -}); - -export default defineComponent({ - extends: widget, - components: { - MkContainer, MkMiniChart - }, - data() { - return { - stats: [], - fetching: true, - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeUnmount() { - clearInterval(this.clock); - }, - methods: { - fetch() { - os.api('hashtags/trend').then(stats => { - this.stats = stats; - this.fetching = false; - }); - } - } -}); -</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> |