From 0508d5f643aeb2ee0cbd1d95c37bc031ad7eb27e Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Tue, 18 Feb 2020 19:31:11 +0900 Subject: [PATCH] Add activity widget --- locales/ja-JP.yml | 1 + src/client/app.vue | 3 +- src/client/widgets/activity.calendar.vue | 84 ++++++++++++++++++ src/client/widgets/activity.chart.vue | 108 +++++++++++++++++++++++ src/client/widgets/activity.vue | 80 +++++++++++++++++ src/client/widgets/index.ts | 1 + 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 src/client/widgets/activity.calendar.vue create mode 100644 src/client/widgets/activity.chart.vue create mode 100644 src/client/widgets/activity.vue diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1d8b09b1c7..f1271c809f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -526,6 +526,7 @@ _widgets: trends: "トレンド" clock: "時計" rss: "RSSリーダー" + activity: "アクティビティ" _cw: hide: "隠す" diff --git a/src/client/app.vue b/src/client/app.vue index a23b6e1289..e88979f001 100644 --- a/src/client/app.vue +++ b/src/client/app.vue @@ -606,7 +606,8 @@ export default Vue.extend({ 'calendar', 'rss', 'trends', - 'clock' + 'clock', + 'activity', ]; this.$root.menu({ diff --git a/src/client/widgets/activity.calendar.vue b/src/client/widgets/activity.calendar.vue new file mode 100644 index 0000000000..dfc0d29d3b --- /dev/null +++ b/src/client/widgets/activity.calendar.vue @@ -0,0 +1,84 @@ +<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 Vue from 'vue'; + +export default Vue.extend({ + 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 new file mode 100644 index 0000000000..0278e02ae7 --- /dev/null +++ b/src/client/widgets/activity.chart.vue @@ -0,0 +1,108 @@ +<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 Vue from 'vue'; +import i18n from '../i18n'; + +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 Vue.extend({ + i18n, + 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 new file mode 100644 index 0000000000..5f18c17d48 --- /dev/null +++ b/src/client/widgets/activity.vue @@ -0,0 +1,80 @@ +<template> +<div> + <mk-container :show-header="props.design === 0" :naked="props.design === 2"> + <template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template> + <template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template> + + <div class=""> + <mk-loading v-if="fetching"/> + <template v-else> + <x-calendar v-show="props.view === 0" :data="[].concat(activity)"/> + <x-chart v-show="props.view === 1" :data="[].concat(activity)"/> + </template> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import { faChartBar, faSort } from '@fortawesome/free-solid-svg-icons'; +import MkContainer from '../components/ui/container.vue'; +import define from './define'; +import i18n from '../i18n'; +import XCalendar from './activity.calendar.vue'; +import XChart from './activity.chart.vue'; + +export default define({ + name: 'activity', + props: () => ({ + design: 0, + view: 0 + }) +}).extend({ + i18n, + components: { + MkContainer, + XCalendar, + XChart, + }, + data() { + return { + fetching: true, + activity: null, + faChartBar, faSort + }; + }, + mounted() { + this.$root.api('charts/user/notes', { + userId: this.$store.state.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: { + func() { + if (this.props.design == 2) { + this.props.design = 0; + } else { + this.props.design++; + } + this.save(); + }, + toggleView() { + if (this.props.view == 1) { + this.props.view = 0; + } else { + this.props.view++; + } + this.save(); + } + } +}); +</script> diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts index d6af41e2f8..9f8bbc8882 100644 --- a/src/client/widgets/index.ts +++ b/src/client/widgets/index.ts @@ -7,3 +7,4 @@ Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default)); Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default)); Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default)); +Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));