diff --git a/src/client/app.vue b/src/client/app.vue index c2e60c9bbe..615f6b9cd3 100644 --- a/src/client/app.vue +++ b/src/client/app.vue @@ -293,7 +293,7 @@ export default Vue.extend({ const ro = new ResizeObserver((entries, observer) => { adjustTitlePosition(); }); - + ro.observe(this.$refs.contents); window.addEventListener('resize', adjustTitlePosition); @@ -556,6 +556,7 @@ export default Vue.extend({ 'calendar', 'rss', 'trends', + 'clock' ]; this.$root.menu({ diff --git a/src/client/components/analog-clock.vue b/src/client/components/analog-clock.vue new file mode 100644 index 0000000000..a107362240 --- /dev/null +++ b/src/client/components/analog-clock.vue @@ -0,0 +1,143 @@ +<template> +<svg class="mk-analog-clock" viewBox="0 0 10 10" preserveAspectRatio="none"> + <circle v-for="angle, i in graduations" + :cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" + :cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" + :r="i % 5 == 0 ? 0.125 : 0.05" + :fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"/> + + <line + :x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))" + :y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))" + :x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))" + :y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))" + :stroke="sHandColor" + stroke-width="0.05"/> + + <line + :x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))" + :y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))" + :x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))" + :y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))" + :stroke="mHandColor" + stroke-width="0.1"/> + + <line + :x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))" + :y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))" + :x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))" + :y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))" + :stroke="hHandColor" + stroke-width="0.1"/> +</svg> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as tinycolor from 'tinycolor2'; + +export default Vue.extend({ + props: { + dark: { + type: Boolean, + default: false + }, + smooth: { + type: Boolean, + default: false + } + }, + + data() { + return { + now: new Date(), + enabled: true, + + graduationsPadding: 0.5, + handsPadding: 1, + handsTailLength: 0.7, + hHandLengthRatio: 0.75, + mHandLengthRatio: 1, + sHandLengthRatio: 1 + }; + }, + + computed: { + majorGraduationColor(): string { + return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; + }, + minorGraduationColor(): string { + return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + }, + + sHandColor(): string { + return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; + }, + mHandColor(): string { + return this.dark ? '#fff' : '#777'; + }, + hHandColor(): string { + return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString(); + }, + + ms(): number { + return this.now.getMilliseconds() * this.smooth; + }, + s(): number { + return this.now.getSeconds(); + }, + m(): number { + return this.now.getMinutes(); + }, + h(): number { + return this.now.getHours(); + }, + + hAngle(): number { + return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6; + }, + mAngle(): number { + return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30; + }, + sAngle(): number { + return Math.PI * (this.s + this.ms / 1000) / 30; + }, + + graduations(): any { + const angles = []; + for (let i = 0; i < 60; i++) { + const angle = Math.PI * i / 30; + angles.push(angle); + } + + return angles; + } + }, + + mounted() { + const update = () => { + if (this.enabled) { + this.tick(); + requestAnimationFrame(update); + } + }; + update(); + }, + + beforeDestroy() { + this.enabled = false; + }, + + methods: { + tick() { + this.now = new Date(); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-analog-clock { + display: block; +} +</style> diff --git a/src/client/widgets/clock.vue b/src/client/widgets/clock.vue new file mode 100644 index 0000000000..bd521813b7 --- /dev/null +++ b/src/client/widgets/clock.vue @@ -0,0 +1,42 @@ +<template> +<div class="mkw-clock"> + <mk-container :naked="props.style % 2 === 0" :show-header="false"> + <div class="mkw-analog-clock--body"> + <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import define from './define'; + +import MkContainer from '../components/ui/container.vue'; +import MkAnalogClock from '../components/analog-clock.vue'; + +export default define({ + name: 'clock', + props: () => ({ + style: 0 + }) +}).extend({ + components: { + MkContainer, + MkAnalogClock + }, + methods: { + func() { + this.props.style = (this.props.style + 1) % 4; + this.save(); + } + } +}); +</script> + +<style lang="scss" scoped> +.mkw-analog-clock { + .mkw-analog-clock--body { + padding: 8px; + } +} +</style> diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts index 4743be0763..d6af41e2f8 100644 --- a/src/client/widgets/index.ts +++ b/src/client/widgets/index.ts @@ -6,3 +6,4 @@ Vue.component('mkw-timeline', () => import('./timeline.vue').then(m => m.default 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));