enhance(client): improve clock widgets
This commit is contained in:
		
							parent
							
								
									bdaa35d11f
								
							
						
					
					
						commit
						2cd70b80a2
					
				
					 4 changed files with 203 additions and 31 deletions
				
			
		|  | @ -1,13 +1,29 @@ | ||||||
| <template> | <template> | ||||||
| <svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none"> | <svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none"> | ||||||
| 	<circle v-for="(angle, i) in graduations" | 	<circle | ||||||
| 					:key="i" | 		v-for="(angle, i) in graduations" | ||||||
| 					:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" | 		:key="i" | ||||||
| 					:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" | 		:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" | ||||||
| 					:r="i % 5 == 0 ? 0.125 : 0.05" | 		:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" | ||||||
| 					:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor" | 		:r="i % 5 == 0 ? 0.125 : 0.05" | ||||||
|  | 		:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor" | ||||||
| 	/> | 	/> | ||||||
| 
 | 
 | ||||||
|  | 	<template v-if="props.numbers"> | ||||||
|  | 		<text | ||||||
|  | 			v-for="(angle, i) in texts" | ||||||
|  | 			:x="5 + (Math.sin(angle) * (5 - textsPadding))" | ||||||
|  | 			:y="5 - (Math.cos(angle) * (5 - textsPadding))" | ||||||
|  | 			text-anchor="middle" | ||||||
|  | 			dominant-baseline="middle" | ||||||
|  | 			font-family="Verdana" | ||||||
|  | 			font-size="0.75" | ||||||
|  | 			fill="currentColor" | ||||||
|  | 		> | ||||||
|  | 			{{ i === 0 ? (props.twentyfour ? '24' : '12') : i }} | ||||||
|  | 		</text> | ||||||
|  | 	</template> | ||||||
|  | 
 | ||||||
| 	<line | 	<line | ||||||
| 		:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))" | 		:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))" | ||||||
| 		:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))" | 		:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))" | ||||||
|  | @ -44,22 +60,50 @@ | ||||||
| import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; | import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; | ||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
| 
 | 
 | ||||||
| withDefaults(defineProps<{ | const graduationsPadding = 0.5; | ||||||
| 	thickness: number; | const textsPadding = 0.5; | ||||||
|  | const handsPadding = 1; | ||||||
|  | const handsTailLength = 0.7; | ||||||
|  | const hHandLengthRatio = 0.75; | ||||||
|  | const mHandLengthRatio = 1; | ||||||
|  | const sHandLengthRatio = 1; | ||||||
|  | const graduations = (() => { | ||||||
|  | 	const angles: number[] = []; | ||||||
|  | 	for (let i = 0; i < 60; i++) { | ||||||
|  | 		const angle = Math.PI * i / 30; | ||||||
|  | 		angles.push(angle); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return angles; | ||||||
|  | })(); | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ | ||||||
|  | 	numbers?: boolean; | ||||||
|  | 	thickness?: number; | ||||||
|  | 	offset?: number; | ||||||
|  | 	twentyfour?: boolean; | ||||||
| }>(), { | }>(), { | ||||||
|  | 	numbers: false, | ||||||
| 	thickness: 0.1, | 	thickness: 0.1, | ||||||
|  | 	offset: 0 - new Date().getTimezoneOffset(), | ||||||
|  | 	twentyfour: false, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const texts = computed(() => { | ||||||
|  | 	const angles: number[] = []; | ||||||
|  | 	const times = props.twentyfour ? 24 : 12; | ||||||
|  | 	for (let i = 0; i < times; i++) { | ||||||
|  | 		const angle = Math.PI * i / (times / 2); | ||||||
|  | 		angles.push(angle); | ||||||
|  | 	} | ||||||
|  | 	return angles; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const now = ref(new Date()); | const now = ref(new Date()); | ||||||
| const enabled = ref(true); | now.value.setMinutes(now.value.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); | ||||||
| const graduationsPadding = ref(0.5); |  | ||||||
| const handsPadding = ref(1); |  | ||||||
| const handsTailLength = ref(0.7); |  | ||||||
| const hHandLengthRatio = ref(0.75); |  | ||||||
| const mHandLengthRatio = ref(1); |  | ||||||
| const sHandLengthRatio = ref(1); |  | ||||||
| const computedStyle = getComputedStyle(document.documentElement); |  | ||||||
| 
 | 
 | ||||||
|  | const enabled = ref(true); | ||||||
|  | const computedStyle = getComputedStyle(document.documentElement); | ||||||
| const dark = computed(() => tinycolor(computedStyle.getPropertyValue('--bg')).isDark()); | const dark = computed(() => tinycolor(computedStyle.getPropertyValue('--bg')).isDark()); | ||||||
| const majorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'); | const majorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'); | ||||||
| const minorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'); | const minorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'); | ||||||
|  | @ -69,21 +113,13 @@ const hHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--ac | ||||||
| const s = computed(() => now.value.getSeconds()); | const s = computed(() => now.value.getSeconds()); | ||||||
| const m = computed(() => now.value.getMinutes()); | const m = computed(() => now.value.getMinutes()); | ||||||
| const h = computed(() => now.value.getHours()); | const h = computed(() => now.value.getHours()); | ||||||
| const hAngle = computed(() => Math.PI * (h.value % 12 + (m.value + s.value / 60) / 60) / 6); | const hAngle = computed(() => Math.PI * (h.value % (props.twentyfour ? 24 : 12) + (m.value + s.value / 60) / 60) / (props.twentyfour ? 12 : 6)); | ||||||
| const mAngle = computed(() => Math.PI * (m.value + s.value / 60) / 30); | const mAngle = computed(() => Math.PI * (m.value + s.value / 60) / 30); | ||||||
| const sAngle = computed(() => Math.PI * s.value / 30); | const sAngle = computed(() => Math.PI * s.value / 30); | ||||||
| const graduations = computed(() => { |  | ||||||
| 	const angles: number[] = []; |  | ||||||
| 	for (let i = 0; i < 60; i++) { |  | ||||||
| 		const angle = Math.PI * i / 30; |  | ||||||
| 		angles.push(angle); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return angles; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| function tick() { | function tick() { | ||||||
| 	now.value = new Date(); | 	now.value = new Date(); | ||||||
|  | 	now.value.setMinutes(now.value.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|  |  | ||||||
							
								
								
									
										49
									
								
								packages/client/src/scripts/timezones.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								packages/client/src/scripts/timezones.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | ||||||
|  | export const timezones = [{ | ||||||
|  | 	name: 'UTC', | ||||||
|  | 	abbrev: 'UTC', | ||||||
|  | 	offset: 0, | ||||||
|  | }, { | ||||||
|  | 	name: 'Europe/Berlin', | ||||||
|  | 	abbrev: 'CET', | ||||||
|  | 	offset: 60, | ||||||
|  | }, { | ||||||
|  | 	name: 'Asia/Tokyo', | ||||||
|  | 	abbrev: 'JST', | ||||||
|  | 	offset: 540, | ||||||
|  | }, { | ||||||
|  | 	name: 'Asia/Seoul', | ||||||
|  | 	abbrev: 'KST', | ||||||
|  | 	offset: 540, | ||||||
|  | }, { | ||||||
|  | 	name: 'Asia/Shanghai', | ||||||
|  | 	abbrev: 'CST', | ||||||
|  | 	offset: 480, | ||||||
|  | }, { | ||||||
|  | 	name: 'Australia/Sydney', | ||||||
|  | 	abbrev: 'AEST', | ||||||
|  | 	offset: 600, | ||||||
|  | }, { | ||||||
|  | 	name: 'Australia/Darwin', | ||||||
|  | 	abbrev: 'ACST', | ||||||
|  | 	offset: 570, | ||||||
|  | }, { | ||||||
|  | 	name: 'Australia/Perth', | ||||||
|  | 	abbrev: 'AWST', | ||||||
|  | 	offset: 480, | ||||||
|  | }, { | ||||||
|  | 	name: 'America/New_York', | ||||||
|  | 	abbrev: 'EST', | ||||||
|  | 	offset: -300, | ||||||
|  | }, { | ||||||
|  | 	name: 'America/Mexico_City', | ||||||
|  | 	abbrev: 'CST', | ||||||
|  | 	offset: -360, | ||||||
|  | }, { | ||||||
|  | 	name: 'America/Phoenix', | ||||||
|  | 	abbrev: 'MST', | ||||||
|  | 	offset: -420, | ||||||
|  | }, { | ||||||
|  | 	name: 'America/Los_Angeles', | ||||||
|  | 	abbrev: 'PST', | ||||||
|  | 	offset: -480, | ||||||
|  | }]; | ||||||
|  | @ -1,17 +1,20 @@ | ||||||
| <template> | <template> | ||||||
| <MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock"> | <MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock"> | ||||||
| 	<div class="vubelbmv"> | 	<div class="vubelbmv"> | ||||||
| 		<MkAnalogClock class="clock" :thickness="widgetProps.thickness"/> | 		<div v-if="widgetProps.showLabel" class="label abbrev">{{ tzAbbrev }}</div> | ||||||
|  | 		<MkAnalogClock class="clock" :thickness="widgetProps.thickness" :offset="tzOffset" :numbers="widgetProps.numbers" :twentyfour="widgetProps.twentyFour"/> | ||||||
|  | 		<div v-if="widgetProps.showLabel" class="label offset">{{ tzOffsetLabel }}</div> | ||||||
| 	</div> | 	</div> | ||||||
| </MkContainer> | </MkContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
| import { GetFormResultType } from '@/scripts/form'; |  | ||||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||||
|  | import { GetFormResultType } from '@/scripts/form'; | ||||||
| import MkContainer from '@/components/ui/container.vue'; | import MkContainer from '@/components/ui/container.vue'; | ||||||
| import MkAnalogClock from '@/components/analog-clock.vue'; | import MkAnalogClock from '@/components/analog-clock.vue'; | ||||||
|  | import { timezones } from '@/scripts/timezones'; | ||||||
| 
 | 
 | ||||||
| const name = 'clock'; | const name = 'clock'; | ||||||
| 
 | 
 | ||||||
|  | @ -24,11 +27,34 @@ const widgetPropsDef = { | ||||||
| 		type: 'radio' as const, | 		type: 'radio' as const, | ||||||
| 		default: 0.1, | 		default: 0.1, | ||||||
| 		options: [{ | 		options: [{ | ||||||
| 			value: 0.1, label: 'thin' | 			value: 0.1, label: 'thin', | ||||||
| 		}, { | 		}, { | ||||||
| 			value: 0.2, label: 'medium' | 			value: 0.2, label: 'medium', | ||||||
| 		}, { | 		}, { | ||||||
| 			value: 0.3, label: 'thick' | 			value: 0.3, label: 'thick', | ||||||
|  | 		}], | ||||||
|  | 	}, | ||||||
|  | 	numbers: { | ||||||
|  | 		type: 'boolean' as const, | ||||||
|  | 		default: false, | ||||||
|  | 	}, | ||||||
|  | 	twentyFour: { | ||||||
|  | 		type: 'boolean' as const, | ||||||
|  | 		default: false, | ||||||
|  | 	}, | ||||||
|  | 	showLabel: { | ||||||
|  | 		type: 'boolean' as const, | ||||||
|  | 		default: true, | ||||||
|  | 	}, | ||||||
|  | 	timezone: { | ||||||
|  | 		type: 'enum' as const, | ||||||
|  | 		default: null, | ||||||
|  | 		enum: [...timezones.map((tz) => ({ | ||||||
|  | 			label: tz.name, | ||||||
|  | 			value: tz.name.toLowerCase(), | ||||||
|  | 		})), { | ||||||
|  | 			label: '(auto)', | ||||||
|  | 			value: null, | ||||||
| 		}], | 		}], | ||||||
| 	}, | 	}, | ||||||
| }; | }; | ||||||
|  | @ -47,6 +73,16 @@ const { widgetProps, configure } = useWidgetPropsManager(name, | ||||||
| 	emit, | 	emit, | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | const tzAbbrev = $computed(() => (widgetProps.timezone === null | ||||||
|  | 	? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev | ||||||
|  | 	: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?'); | ||||||
|  | 
 | ||||||
|  | const tzOffset = $computed(() => widgetProps.timezone === null | ||||||
|  | 	? 0 - new Date().getTimezoneOffset() | ||||||
|  | 	: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0); | ||||||
|  | 
 | ||||||
|  | const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0')); | ||||||
|  | 
 | ||||||
| defineExpose<WidgetComponentExpose>({ | defineExpose<WidgetComponentExpose>({ | ||||||
| 	name, | 	name, | ||||||
| 	configure, | 	configure, | ||||||
|  | @ -57,6 +93,23 @@ defineExpose<WidgetComponentExpose>({ | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .vubelbmv { | .vubelbmv { | ||||||
| 	padding: 8px; | 	padding: 8px; | ||||||
|  | 	position: relative; | ||||||
|  | 
 | ||||||
|  | 	> .label { | ||||||
|  | 		opacity: 0.7; | ||||||
|  | 
 | ||||||
|  | 		&.abbrev { | ||||||
|  | 			position: absolute; | ||||||
|  | 			top: 14px; | ||||||
|  | 			left: 14px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		&.offset { | ||||||
|  | 			position: absolute; | ||||||
|  | 			bottom: 14px; | ||||||
|  | 			right: 14px; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	> .clock { | 	> .clock { | ||||||
| 		height: 150px; | 		height: 150px; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }"> | <div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }"> | ||||||
|  | 	<div v-if="widgetProps.showLabel" class="label">{{ tzAbbrev }}</div> | ||||||
| 	<div class="time"> | 	<div class="time"> | ||||||
| 		<span v-text="hh"></span> | 		<span v-text="hh"></span> | ||||||
| 		<span class="colon" :class="{ showColon }">:</span> | 		<span class="colon" :class="{ showColon }">:</span> | ||||||
|  | @ -9,6 +10,7 @@ | ||||||
| 		<span v-if="widgetProps.showMs" class="colon" :class="{ showColon }">:</span> | 		<span v-if="widgetProps.showMs" class="colon" :class="{ showColon }">:</span> | ||||||
| 		<span v-if="widgetProps.showMs" v-text="ms"></span> | 		<span v-if="widgetProps.showMs" v-text="ms"></span> | ||||||
| 	</div> | 	</div> | ||||||
|  | 	<div v-if="widgetProps.showLabel" class="label">{{ tzOffsetLabel }}</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -16,6 +18,7 @@ | ||||||
| import { onUnmounted, ref, watch } from 'vue'; | import { onUnmounted, ref, watch } from 'vue'; | ||||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||||
| import { GetFormResultType } from '@/scripts/form'; | import { GetFormResultType } from '@/scripts/form'; | ||||||
|  | import { timezones } from '@/scripts/timezones'; | ||||||
| 
 | 
 | ||||||
| const name = 'digitalClock'; | const name = 'digitalClock'; | ||||||
| 
 | 
 | ||||||
|  | @ -33,6 +36,21 @@ const widgetPropsDef = { | ||||||
| 		type: 'boolean' as const, | 		type: 'boolean' as const, | ||||||
| 		default: true, | 		default: true, | ||||||
| 	}, | 	}, | ||||||
|  | 	showLabel: { | ||||||
|  | 		type: 'boolean' as const, | ||||||
|  | 		default: true, | ||||||
|  | 	}, | ||||||
|  | 	timezone: { | ||||||
|  | 		type: 'enum' as const, | ||||||
|  | 		default: null, | ||||||
|  | 		enum: [...timezones.map((tz) => ({ | ||||||
|  | 			label: tz.name, | ||||||
|  | 			value: tz.name.toLowerCase(), | ||||||
|  | 		})), { | ||||||
|  | 			label: '(auto)', | ||||||
|  | 			value: null, | ||||||
|  | 		}], | ||||||
|  | 	}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | type WidgetProps = GetFormResultType<typeof widgetPropsDef>; | ||||||
|  | @ -49,6 +67,16 @@ const { widgetProps, configure } = useWidgetPropsManager(name, | ||||||
| 	emit, | 	emit, | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | const tzAbbrev = $computed(() => (widgetProps.timezone === null | ||||||
|  | 	? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev | ||||||
|  | 	: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?'); | ||||||
|  | 
 | ||||||
|  | const tzOffset = $computed(() => widgetProps.timezone === null | ||||||
|  | 	? 0 - new Date().getTimezoneOffset() | ||||||
|  | 	: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0); | ||||||
|  | 
 | ||||||
|  | const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0')); | ||||||
|  | 
 | ||||||
| let intervalId; | let intervalId; | ||||||
| const hh = ref(''); | const hh = ref(''); | ||||||
| const mm = ref(''); | const mm = ref(''); | ||||||
|  | @ -67,6 +95,7 @@ watch(showColon, (v) => { | ||||||
| 
 | 
 | ||||||
| const tick = () => { | const tick = () => { | ||||||
| 	const now = new Date(); | 	const now = new Date(); | ||||||
|  | 	now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + tzOffset)); | ||||||
| 	hh.value = now.getHours().toString().padStart(2, '0'); | 	hh.value = now.getHours().toString().padStart(2, '0'); | ||||||
| 	mm.value = now.getMinutes().toString().padStart(2, '0'); | 	mm.value = now.getMinutes().toString().padStart(2, '0'); | ||||||
| 	ss.value = now.getSeconds().toString().padStart(2, '0'); | 	ss.value = now.getSeconds().toString().padStart(2, '0'); | ||||||
|  | @ -98,6 +127,11 @@ defineExpose<WidgetComponentExpose>({ | ||||||
| 	padding: 16px 0; | 	padding: 16px 0; | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
| 
 | 
 | ||||||
|  | 	> .label { | ||||||
|  | 		font-size: 65%; | ||||||
|  | 		opacity: 0.7; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	> .time { | 	> .time { | ||||||
| 		> .colon { | 		> .colon { | ||||||
| 			opacity: 0; | 			opacity: 0; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue