ENH - clock app

This commit is contained in:
Eichenholz
2026-08-03 20:18:41 +02:00
parent bda7b32080
commit e64bddcc15
5 changed files with 222 additions and 241 deletions
+54 -140
View File
@@ -697,76 +697,46 @@ button {
padding-left: 27px;
}
.clock-content {
height: calc(100% - 104px);
overflow: auto;
padding: 12px 15px 65px;
min-height: calc(100% - 113px);
padding: 0 0 88px;
}
.settings-list {
overflow: hidden;
border-radius: 12px;
background: #1c1c1e;
.clock-konsta-list {
margin-top: 0;
margin-bottom: 0;
}
.settings-row {
min-height: 54px;
margin-left: 14px;
padding: 9px 14px 9px 0;
.clock-world-now {
margin: 0;
padding: 76px 16px 0;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #ffffff14;
}
.settings-row:last-child {
border-bottom: 0;
}
.settings-row > span:nth-child(2) {
flex: 1;
}
.settings-row strong:last-child {
margin-left: auto;
font-size: 22px;
}
.settings-row > div {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
text-align: center;
}
.alarm-time {
font-size: 30px;
font-weight: 300;
.clock-world-location {
color: #8e8e93;
font-size: 19px;
font-weight: 500;
}
.settings-row small {
color: #999;
.clock-world-time {
margin-top: 8px;
font-variant-numeric: tabular-nums;
font-size: min(76px, 20cqw);
font-weight: 200;
letter-spacing: -4px;
}
.ios-switch {
margin-left: auto;
width: 46px;
height: 27px;
padding: 2px;
border: 0;
border-radius: 20px;
background: #3a3a3d;
transition: 0.2s;
.clock-alarm-list {
margin-top: 8px;
}
.ios-switch span {
display: block;
width: 23px;
height: 23px;
border-radius: 50%;
background: white;
transition: 0.2s;
box-shadow: 0 1px 4px #0007;
}
.ios-switch.active {
background: #30d158;
}
.ios-switch.active span {
transform: translateX(19px);
.clock-alarm-item {
min-height: 72px;
}
.clock-tool {
margin: 0;
padding: 0 16px;
text-align: center;
}
.clock-digits {
margin: 60px 0;
margin: 70px 0 54px;
font-variant-numeric: tabular-nums;
font-size: 45px;
font-weight: 250;
@@ -774,30 +744,21 @@ button {
.clock-actions {
display: flex;
justify-content: space-between;
margin: 15px;
margin: 12px 0;
}
.clock-actions button {
.clock-action-button {
width: 66px;
height: 66px;
border: 0;
border-radius: 50%;
background: #2c2c2e;
color: white;
min-width: 66px;
padding: 0;
}
.clock-actions button.positive {
background: #103a20;
color: #30d158;
}
.lap-list > div {
display: flex;
justify-content: space-between;
padding: 9px;
border-top: 1px solid #ffffff20;
.clock-lap-list {
margin-top: 24px;
}
.timer-ring {
width: 190px;
height: 190px;
margin: 28px auto;
width: 170px;
height: 170px;
margin: 12px auto 16px;
display: grid;
place-items: center;
border: 6px solid #ff9f0a33;
@@ -805,18 +766,11 @@ button {
border-radius: 50%;
font-size: 34px;
}
.timer-input {
display: flex;
justify-content: center;
gap: 10px;
color: #aaa;
.clock-timer-input {
margin-right: 0;
margin-left: 0;
}
.timer-input input {
width: 55px;
border: 0;
border-radius: 8px;
background: #252527;
color: white;
.clock-timer-input input {
text-align: center;
}
.camera-app {
@@ -1255,65 +1209,25 @@ button {
.reference-tabbar button.active {
color: #0a84ff;
}
.clock-app .app-tabs button.active {
color: #d99900;
}
.clock-app {
padding-top: 0;
padding: 44px 0 24px;
background: #020202;
}
.reference-clock-header {
padding: 47px 16px 0;
.clock-tabbar {
position: absolute !important;
right: 0;
bottom: 24px;
left: 0;
}
.reference-clock-header nav {
display: flex;
justify-content: space-between;
color: #d99900;
.clock-tabbar .k-link {
min-width: 0 !important;
padding-right: 4px;
padding-left: 4px;
outline: none;
}
.reference-clock-header nav button {
padding: 0;
border: 0;
background: transparent;
color: inherit;
font-size: 15px;
}
.reference-clock-header h1 {
margin: 14px 0 0;
padding-bottom: 11px;
border-bottom: 1px solid #ffffff1c;
font-size: 35px;
line-height: 1;
}
.clock-app .clock-content {
height: calc(100% - 155px);
padding: 0 16px 72px;
}
.world-clock-list > div {
min-height: 95px;
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff1c;
}
.world-clock-list > div > div {
flex: 1;
display: flex;
flex-direction: column;
}
.world-clock-list small {
color: #8e8e93;
font-size: 11px;
}
.world-clock-list strong {
font-size: min(25px, 7cqw);
font-weight: 400;
}
.world-clock-list time {
font-size: min(42px, 11cqw);
font-weight: 200;
letter-spacing: -2px;
}
.reference-clock-tabs button.active {
color: #d99900;
.clock-tab-label {
font-size: 9px;
letter-spacing: -0.1px;
}
.reference-photos {
+1 -8
View File
@@ -80,20 +80,13 @@ const defaultLocales: LocaleTree = {
lap: 'Lap',
minutes: 'Minutes',
add: 'Add clock',
today: 'Today',
offset: '+0HRS',
location: 'Los Santos',
tabs: {
world: 'World Clock',
alarm: 'Alarm',
stopwatch: 'Stopwatch',
timer: 'Timer',
},
cities: {
cupertino: 'Cupertino',
newYork: 'New York',
london: 'London',
tokyo: 'Tokyo',
},
alarm: { weekday: 'Weekdays', weekend: 'Weekend' },
},
photos: {
+164 -89
View File
@@ -1,6 +1,19 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import {
kBlock,
kButton,
kLink,
kList,
kListInput,
kListItem,
kNavbar,
kPage,
kTabbar,
kTabbarLink,
kToggle,
} from 'konsta/vue'
import { AlarmClock, Clock3, Plus, Timer, TimerReset } from 'lucide-vue-next'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useClockStore } from '@/stores/clock'
import { usePhoneStore } from '@/stores/phone'
@@ -16,6 +29,7 @@ const clock = useClockStore()
const tab = ref<'world' | 'alarm' | 'stopwatch' | 'timer'>('world')
const now = ref(Date.now())
let ticker: ReturnType<typeof setInterval> | undefined
const stopwatchValue = computed(() =>
elapsedMilliseconds(
clock.stopwatchAccumulated,
@@ -30,77 +44,113 @@ const timerValue = computed(() =>
now.value,
),
)
const cities = [
{ key: 'cupertino', zone: 'America/Los_Angeles' },
{ key: 'newYork', zone: 'America/New_York' },
{ key: 'london', zone: 'Europe/London' },
{ key: 'tokyo', zone: 'Asia/Tokyo' },
]
const losSantosTime = computed(() =>
new Intl.DateTimeFormat(phone.lang, {
hour: '2-digit',
minute: '2-digit',
timeZone: 'America/Los_Angeles',
}).format(now.value),
)
const tabs = [
{ id: 'world', icon: Clock3 },
{ id: 'alarm', icon: AlarmClock },
{ id: 'stopwatch', icon: Timer },
{ id: 'timer', icon: TimerReset },
] as const
function cityTime(zone: string): string {
return new Intl.DateTimeFormat(phone.lang, {
hour: '2-digit',
minute: '2-digit',
timeZone: zone,
}).format(now.value)
const secondaryActionColors = {
tonalBgIos: 'bg-[#2c2c2e] active:bg-[#3a3a3c]',
tonalTextIos: 'text-white',
}
const positiveActionColors = {
fillBgIos: 'bg-[#103a20] active:bg-[#174d2a]',
fillTextIos: 'text-[#30d158]',
}
const toggleColors = {
checkedBgIos: 'bg-[#30d158]',
}
const tabColors = {
textActiveIos: 'text-[#d99900]',
textIos: 'text-[#77777c]',
}
onMounted(() => {
ticker = setInterval(() => {
now.value = Date.now()
}, 250)
})
onBeforeUnmount(() => clearInterval(ticker))
</script>
<template>
<main class="native-app clock-app">
<header class="reference-clock-header">
<nav>
<button type="button">{{ phone.t('Common.edit') }}</button
><button type="button" :aria-label="phone.t('Apps.clock.add')">
<Plus :size="25" />
</button>
</nav>
<h1>{{ phone.t(`Apps.clock.tabs.${tab}`) }}</h1>
</header>
<section class="clock-content">
<div v-if="tab === 'world'" class="world-clock-list">
<div v-for="city in cities" :key="city.key">
<div>
<small
>{{ phone.t('Apps.clock.today') }},
{{ phone.t('Apps.clock.offset') }}</small
><strong>{{ phone.t(`Apps.clock.cities.${city.key}`) }}</strong>
</div>
<time>{{ cityTime(city.zone) }}</time>
</div>
</div>
<div v-else-if="tab === 'alarm'" class="settings-list">
<div v-for="alarm in clock.alarms" :key="alarm.id" class="settings-row">
<div>
<strong class="alarm-time">{{ alarm.time }}</strong
><small>{{ phone.t(alarm.labelKey) }}</small>
</div>
<button
class="ios-switch"
:class="{ active: alarm.enabled }"
type="button"
@click="clock.toggleAlarm(alarm.id)"
>
<span />
</button>
</div>
</div>
<div v-else-if="tab === 'stopwatch'" class="clock-tool">
<k-page component="main" class="native-app clock-app">
<k-navbar
:title="phone.t(`Apps.clock.tabs.${tab}`)"
:large="tab === 'world' || tab === 'alarm'"
:transparent="tab === 'world' || tab === 'alarm'"
class="clock-navbar"
>
<template #left>
<k-link
component="button"
:link-props="{ type: 'button' }"
>
{{ phone.t('Common.edit') }}
</k-link>
</template>
<template #right>
<k-link
component="button"
icon-only
:link-props="{ type: 'button' }"
:aria-label="phone.t('Apps.clock.add')"
>
<Plus />
</k-link>
</template>
</k-navbar>
<k-block component="section" nested class="clock-content">
<k-block v-if="tab === 'world'" nested class="clock-world-now">
<span class="clock-world-location">{{
phone.t('Apps.clock.location')
}}</span>
<time class="clock-world-time">{{ losSantosTime }}</time>
</k-block>
<k-list
v-else-if="tab === 'alarm'"
strong
inset
class="clock-konsta-list clock-alarm-list"
>
<k-list-item
v-for="alarm in clock.alarms"
:key="alarm.id"
:title="alarm.time"
:subtitle="phone.t(alarm.labelKey)"
title-font-size-ios="text-[30px]"
class="clock-alarm-item"
>
<template #after>
<k-toggle
:checked="alarm.enabled"
:colors="toggleColors"
:aria-label="`${alarm.time}, ${phone.t(alarm.labelKey)}`"
@change="clock.toggleAlarm(alarm.id)"
/>
</template>
</k-list-item>
</k-list>
<k-block v-else-if="tab === 'stopwatch'" nested class="clock-tool">
<div class="clock-digits">{{ formatStopwatch(stopwatchValue) }}</div>
<div class="clock-actions">
<button
type="button"
<k-button
rounded
tonal
:colors="secondaryActionColors"
class="clock-action-button"
@click="
clock.stopwatchStartedAt === null
? clock.resetStopwatch()
@@ -113,10 +163,12 @@ onBeforeUnmount(() => clearInterval(ticker))
? 'Common.reset'
: 'Apps.clock.lap',
)
}}</button
><button
class="positive"
type="button"
}}
</k-button>
<k-button
rounded
:colors="positiveActionColors"
class="clock-action-button"
@click="
clock.stopwatchStartedAt === null
? clock.startStopwatch(now)
@@ -130,24 +182,25 @@ onBeforeUnmount(() => clearInterval(ticker))
: 'Common.stop',
)
}}
</button>
</k-button>
</div>
<div class="lap-list">
<div v-for="(lap, index) in clock.laps" :key="index">
<span
>{{ phone.t('Apps.clock.lap') }}
{{ clock.laps.length - index }}</span
><span>{{ formatStopwatch(lap) }}</span>
</div>
</div>
</div>
<div v-else class="clock-tool">
<k-list dividers class="clock-konsta-list clock-lap-list">
<k-list-item
v-for="(lap, index) in clock.laps"
:key="index"
:title="`${phone.t('Apps.clock.lap')} ${clock.laps.length - index}`"
:after="formatStopwatch(lap)"
/>
</k-list>
</k-block>
<k-block v-else nested class="clock-tool">
<div class="timer-ring">
<span>{{ formatTimer(timerValue) }}</span>
</div>
<label class="timer-input"
>{{ phone.t('Apps.clock.minutes')
}}<input
<k-list strong inset class="clock-konsta-list clock-timer-input">
<k-list-input
:label="phone.t('Apps.clock.minutes')"
type="number"
min="1"
max="60"
@@ -157,13 +210,22 @@ onBeforeUnmount(() => clearInterval(ticker))
Number(($event.target as HTMLInputElement).value),
)
"
/></label>
/>
</k-list>
<div class="clock-actions">
<button type="button" @click="clock.resetTimer()">
{{ phone.t('Common.reset') }}</button
><button
class="positive"
type="button"
<k-button
rounded
tonal
:colors="secondaryActionColors"
class="clock-action-button"
@click="clock.resetTimer()"
>
{{ phone.t('Common.reset') }}
</k-button>
<k-button
rounded
:colors="positiveActionColors"
class="clock-action-button"
@click="
clock.timerStartedAt === null
? clock.startTimer(now)
@@ -175,22 +237,35 @@ onBeforeUnmount(() => clearInterval(ticker))
clock.timerStartedAt === null ? 'Common.start' : 'Common.pause',
)
}}
</button>
</k-button>
</div>
</div>
</section>
<nav class="reference-tabbar reference-clock-tabs">
<button
</k-block>
</k-block>
<k-tabbar
component="nav"
labels
icons
class="clock-tabbar"
inner-class="!w-full !gap-0"
:aria-label="phone.t('Apps.clock.name')"
>
<k-tabbar-link
v-for="item in tabs"
:key="item.id"
:class="{ active: tab === item.id }"
type="button"
component="button"
:active="tab === item.id"
:colors="tabColors"
:link-props="{ type: 'button' }"
@click="tab = item.id"
>
<component :is="item.icon" :size="22" /><span>{{
<template #icon>
<component :is="item.icon" :size="22" />
</template>
<span class="clock-tab-label">{{
phone.t(`Apps.clock.tabs.${item.id}`)
}}</span>
</button>
</nav>
</main>
</k-tabbar-link>
</k-tabbar>
</k-page>
</template>
+1 -2
View File
@@ -25,9 +25,8 @@ Locales["en"] = {
modes = { timelapse = "Timelapse", slowMo = "Slow-Mo", cinematic = "Cinematic", video = "Video", photo = "Photo", portrait = "Portrait", pano = "Pano" },
},
clock = {
name = "Clock", lap = "Lap", minutes = "Minutes", add = "Add clock", today = "Today", offset = "+0HRS",
name = "Clock", lap = "Lap", minutes = "Minutes", add = "Add clock", location = "Los Santos",
tabs = { world = "World Clock", alarm = "Alarm", stopwatch = "Stopwatch", timer = "Timer" },
cities = { cupertino = "Cupertino", newYork = "New York", london = "London", tokyo = "Tokyo" },
alarm = { weekday = "Weekdays", weekend = "Weekend" },
},
photos = {
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sky Phone</title>
<script type="module" crossorigin src="./assets/sky-index-B21bXyap.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-DIba77eO.css">
<script type="module" crossorigin src="./assets/sky-index-C3leT8Qb.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-BGSbxL7G.css">
</head>
<body>
<div id="app"></div>