From 2cd70b80a21de9bf072af726449480c8d229440c Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 5 Aug 2022 23:51:15 +0900 Subject: [PATCH] enhance(client): improve clock widgets --- .../client/src/components/analog-clock.vue | 88 +++++++++++++------ packages/client/src/scripts/timezones.ts | 49 +++++++++++ packages/client/src/widgets/clock.vue | 63 +++++++++++-- packages/client/src/widgets/digital-clock.vue | 34 +++++++ 4 files changed, 203 insertions(+), 31 deletions(-) create mode 100644 packages/client/src/scripts/timezones.ts diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue index 18dd1e3f4..2ea7680dd 100644 --- a/packages/client/src/components/analog-clock.vue +++ b/packages/client/src/components/analog-clock.vue @@ -1,13 +1,29 @@ <template> <svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none"> - <circle v-for="(angle, i) in graduations" - :key="i" - :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" + <circle + v-for="(angle, i) in graduations" + :key="i" + :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" /> + <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(() => { diff --git a/packages/client/src/scripts/timezones.ts b/packages/client/src/scripts/timezones.ts new file mode 100644 index 000000000..8ce07323f --- /dev/null +++ b/packages/client/src/scripts/timezones.ts @@ -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, +}]; diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue index fbd2f9e89..3b40674e4 100644 --- a/packages/client/src/widgets/clock.vue +++ b/packages/client/src/widgets/clock.vue @@ -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; diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue index 743c5657b..009167421 100644 --- a/packages/client/src/widgets/digital-clock.vue +++ b/packages/client/src/widgets/digital-clock.vue @@ -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;