From dfc358f85a7ee115bd87cc9d687ee28e9833580b Mon Sep 17 00:00:00 2001 From: Eichenholz Date: Tue, 4 Aug 2026 10:58:04 +0200 Subject: [PATCH] ENH - clock app --- frontend/src/App.vue | 23 +- frontend/src/assets/main.css | 75 +++- frontend/src/components/AlarmEditor.vue | 26 +- frontend/src/components/TimeWheelPicker.vue | 341 ++++++++++++--- frontend/src/stores/clock.ts | 33 +- frontend/src/stores/phone.ts | 13 + frontend/src/utils/clock.test.ts | 14 +- frontend/src/utils/clock.ts | 20 +- frontend/src/views/apps/ClockApp.vue | 437 +++++++++++--------- sky_phone/config/locales/en.lua | 5 + sky_phone/source/html/index.html | 4 +- 11 files changed, 707 insertions(+), 284 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 0464047..2878972 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -20,6 +20,7 @@ import { } from '@/stores/notifications' import { usePhoneStore, type PhoneOpenPayload } from '@/stores/phone' import { nuiCall } from '@/utils/nui' +import { formatTimer } from '@/utils/clock' import SpringboardView from '@/views/SpringboardView.vue' type AppMessage = { @@ -39,7 +40,7 @@ const phoneDeviceStyle = computed(() => ({ const phoneFrameImage = computed( () => PHONE_FRAME_IMAGES[phone.preferences.settings.frame], ) -let alarmTicker: ReturnType | undefined +let clockTicker: ReturnType | undefined function onMessage(event: MessageEvent): void { if (event.data?.type === 'app:open') { @@ -67,8 +68,9 @@ onMounted(() => { systemColorScheme.addEventListener('change', onSystemColorSchemeChange) phone.setSystemDarkMode(systemColorScheme.matches) void nuiCall('ui:ready') - alarmTicker = setInterval(() => { - for (const alarm of clock.dueAlarms(Date.now())) { + clockTicker = setInterval(() => { + const now = Date.now() + for (const alarm of clock.dueAlarms(now)) { notifications.show({ appId: 'clock', critical: true, @@ -79,6 +81,19 @@ onMounted(() => { title: phone.t('Apps.clock.name'), }) } + + const timer = clock.consumeDueTimer(now) + if (timer) { + notifications.show({ + appId: 'clock', + critical: true, + persistent: true, + sound: timer.sound, + subtitle: formatTimer(clock.timerDuration), + text: timer.note || phone.t('Apps.clock.timer.ringing'), + title: phone.t('Apps.clock.name'), + }) + } }, 1000) if (import.meta.env.DEV) phone.open() }) @@ -91,7 +106,7 @@ watch( ) onBeforeUnmount(() => { - if (alarmTicker) clearInterval(alarmTicker) + if (clockTicker) clearInterval(clockTicker) window.removeEventListener('message', onMessage) window.removeEventListener('keydown', onKeydown) systemColorScheme.removeEventListener('change', onSystemColorSchemeChange) diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index ba4cec5..8ae99eb 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -780,9 +780,23 @@ button { height: 200px; margin: 0 16px 16px; display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 64px 64px; + justify-content: center; + gap: 8px; overflow: hidden; } +.time-wheel-picker--duration { + grid-template-columns: repeat(3, 64px); + gap: 0; +} +.time-wheel-picker__field { + position: relative; + width: 64px; + height: 200px; +} +.time-wheel-picker--duration .time-wheel-picker__field { + width: 64px; +} .time-wheel-picker__selection { position: absolute; z-index: 0; @@ -794,20 +808,6 @@ button { background: #2c2c2e; pointer-events: none; } -.time-wheel-picker__separator { - position: absolute; - z-index: 2; - top: 80px; - left: 50%; - height: 40px; - transform: translateX(-50%); - display: grid; - place-items: center; - color: #fff; - font-size: 31px; - font-weight: 300; - pointer-events: none; -} .time-wheel-picker__column { position: relative; z-index: 1; @@ -818,6 +818,9 @@ button { scrollbar-width: none; scroll-snap-type: y mandatory; overscroll-behavior: contain; + touch-action: none; + cursor: grab; + user-select: none; mask-image: linear-gradient( to bottom, transparent 0, @@ -826,6 +829,23 @@ button { transparent 100% ); } +.time-wheel-picker--unit-labels .time-wheel-picker__column { + width: 36px; +} +.time-wheel-picker__unit { + position: absolute; + z-index: 2; + top: 80px; + left: 34px; + height: 40px; + color: #8e8e93; + font-size: 9px; + line-height: 40px; + pointer-events: none; +} +.time-wheel-picker__column:active { + cursor: grabbing; +} .time-wheel-picker__column::-webkit-scrollbar { display: none; } @@ -835,6 +855,7 @@ button { display: block; padding: 0; border: 0; + outline: none; scroll-snap-align: center; background: transparent; color: #8e8e93; @@ -846,6 +867,11 @@ button { color 120ms ease, opacity 120ms ease; } +.time-wheel-picker__value:focus-visible { + border-radius: 8px; + outline: 2px solid #ff9f0a; + outline-offset: -2px; +} .time-wheel-picker__value--selected { color: #fff; opacity: 1; @@ -859,11 +885,20 @@ button { padding: 0 16px; text-align: center; } +.clock-section-heading { + margin: 0; + padding: 4px 0 10px; + text-align: left; + font-size: 34px; + font-weight: 700; + line-height: 41px; + letter-spacing: -0.8px; +} .clock-digits { margin: 70px 0 54px; font-variant-numeric: tabular-nums; - font-size: 45px; - font-weight: 250; + font-size: 58px; + font-weight: 200; } .clock-actions { display: flex; @@ -890,12 +925,12 @@ button { border-radius: 50%; font-size: 34px; } -.clock-timer-input { +.clock-timer-settings { margin-right: 0; margin-left: 0; } -.clock-timer-input input { - text-align: center; +.clock-timer { + padding-bottom: 28px; } .camera-app { padding: 45px 0 25px; diff --git a/frontend/src/components/AlarmEditor.vue b/frontend/src/components/AlarmEditor.vue index 976da3a..2467f90 100644 --- a/frontend/src/components/AlarmEditor.vue +++ b/frontend/src/components/AlarmEditor.vue @@ -9,7 +9,7 @@ import { kListItem, kNavbar, } from 'konsta/vue' -import { Check } from 'lucide-vue-next' +import { Check, X } from 'lucide-vue-next' import { reactive } from 'vue' import { usePhoneStore } from '@/stores/phone' @@ -47,6 +47,9 @@ const dangerColors = { tonalBgIos: 'bg-red-500/15 active:bg-red-500/25', tonalTextIos: 'text-red-500', } +const saveLinkColors = { + navbarTextIos: 'text-[#ff9f0a]', +} function setNote(event: Event): void { draft.note = (event.target as HTMLInputElement).value.slice(0, 80) @@ -71,13 +74,26 @@ function toggleWeekday(weekday: number): void {