enhance(client): improve clock widgets

This commit is contained in:
syuilo 2022-08-05 23:51:15 +09:00
parent 03963e8bdf
commit 2ca066a3ac
4 changed files with 203 additions and 31 deletions

View file

@ -1,6 +1,7 @@
<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
v-for="(angle, i) in graduations"
:key="i" :key="i"
:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" :cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" :cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
@ -8,6 +9,21 @@
:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor" :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(() => {

View 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,
}];

View file

@ -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;

View file

@ -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;