Add activity widget
This commit is contained in:
		
							parent
							
								
									d9986b7a2f
								
							
						
					
					
						commit
						0508d5f643
					
				
					 6 changed files with 276 additions and 1 deletions
				
			
		|  | @ -526,6 +526,7 @@ _widgets: | |||
|   trends: "トレンド" | ||||
|   clock: "時計" | ||||
|   rss: "RSSリーダー" | ||||
|   activity: "アクティビティ" | ||||
| 
 | ||||
| _cw: | ||||
|   hide: "隠す" | ||||
|  |  | |||
|  | @ -606,7 +606,8 @@ export default Vue.extend({ | |||
| 				'calendar', | ||||
| 				'rss', | ||||
| 				'trends', | ||||
| 				'clock' | ||||
| 				'clock', | ||||
| 				'activity', | ||||
| 			]; | ||||
| 
 | ||||
| 			this.$root.menu({ | ||||
|  |  | |||
							
								
								
									
										84
									
								
								src/client/widgets/activity.calendar.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/client/widgets/activity.calendar.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										108
									
								
								src/client/widgets/activity.chart.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/client/widgets/activity.chart.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										80
									
								
								src/client/widgets/activity.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/client/widgets/activity.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
|  | @ -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)); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue