mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-01 18:58:58 +00:00
ENH - clock app
This commit is contained in:
+54
-140
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user