enhance(client): improve clock widgets
This commit is contained in:
		
							parent
							
								
									bdaa35d11f
								
							
						
					
					
						commit
						2cd70b80a2
					
				
					 4 changed files with 203 additions and 31 deletions
				
			
		|  | @ -1,6 +1,7 @@ | |||
| <template> | ||||
| <svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none"> | ||||
| 	<circle v-for="(angle, i) in graduations" | ||||
| 	<circle | ||||
| 		v-for="(angle, i) in graduations" | ||||
| 		:key="i" | ||||
| 		:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" | ||||
| 		:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" | ||||
|  | @ -8,6 +9,21 @@ | |||
| 		: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 | ||||
| 		:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))" | ||||
| 		:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))" | ||||
|  | @ -44,22 +60,50 @@ | |||
| import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| 
 | ||||
| withDefaults(defineProps<{ | ||||
| 	thickness: number; | ||||
| const graduationsPadding = 0.5; | ||||
| 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, | ||||
| 	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 enabled = ref(true); | ||||
| 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); | ||||
| now.value.setMinutes(now.value.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); | ||||
| 
 | ||||
| const enabled = ref(true); | ||||
| const computedStyle = getComputedStyle(document.documentElement); | ||||
| 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 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 m = computed(() => now.value.getMinutes()); | ||||
| 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 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() { | ||||
| 	now.value = new Date(); | ||||
| 	now.value.setMinutes(now.value.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); | ||||
| } | ||||
| 
 | ||||
| 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> | ||||
| <MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock"> | ||||
| 	<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> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import MkAnalogClock from '@/components/analog-clock.vue'; | ||||
| import { timezones } from '@/scripts/timezones'; | ||||
| 
 | ||||
| const name = 'clock'; | ||||
| 
 | ||||
|  | @ -24,11 +27,34 @@ const widgetPropsDef = { | |||
| 		type: 'radio' as const, | ||||
| 		default: 0.1, | ||||
| 		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, | ||||
| ); | ||||
| 
 | ||||
| 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>({ | ||||
| 	name, | ||||
| 	configure, | ||||
|  | @ -57,6 +93,23 @@ defineExpose<WidgetComponentExpose>({ | |||
| <style lang="scss" scoped> | ||||
| .vubelbmv { | ||||
| 	padding: 8px; | ||||
| 	position: relative; | ||||
| 
 | ||||
| 	> .label { | ||||
| 		opacity: 0.7; | ||||
| 
 | ||||
| 		&.abbrev { | ||||
| 			position: absolute; | ||||
| 			top: 14px; | ||||
| 			left: 14px; | ||||
| 		} | ||||
| 
 | ||||
| 		&.offset { | ||||
| 			position: absolute; | ||||
| 			bottom: 14px; | ||||
| 			right: 14px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .clock { | ||||
| 		height: 150px; | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <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"> | ||||
| 		<span v-text="hh"></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" v-text="ms"></span> | ||||
| 	</div> | ||||
| 	<div v-if="widgetProps.showLabel" class="label">{{ tzOffsetLabel }}</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -16,6 +18,7 @@ | |||
| import { onUnmounted, ref, watch } from 'vue'; | ||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||
| import { GetFormResultType } from '@/scripts/form'; | ||||
| import { timezones } from '@/scripts/timezones'; | ||||
| 
 | ||||
| const name = 'digitalClock'; | ||||
| 
 | ||||
|  | @ -33,6 +36,21 @@ const widgetPropsDef = { | |||
| 		type: 'boolean' as const, | ||||
| 		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>; | ||||
|  | @ -49,6 +67,16 @@ const { widgetProps, configure } = useWidgetPropsManager(name, | |||
| 	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; | ||||
| const hh = ref(''); | ||||
| const mm = ref(''); | ||||
|  | @ -67,6 +95,7 @@ watch(showColon, (v) => { | |||
| 
 | ||||
| const tick = () => { | ||||
| 	const now = new Date(); | ||||
| 	now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + tzOffset)); | ||||
| 	hh.value = now.getHours().toString().padStart(2, '0'); | ||||
| 	mm.value = now.getMinutes().toString().padStart(2, '0'); | ||||
| 	ss.value = now.getSeconds().toString().padStart(2, '0'); | ||||
|  | @ -98,6 +127,11 @@ defineExpose<WidgetComponentExpose>({ | |||
| 	padding: 16px 0; | ||||
| 	text-align: center; | ||||
| 
 | ||||
| 	> .label { | ||||
| 		font-size: 65%; | ||||
| 		opacity: 0.7; | ||||
| 	} | ||||
| 
 | ||||
| 	> .time { | ||||
| 		> .colon { | ||||
| 			opacity: 0; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue