mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
ENH - clock app
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
kBlock,
|
||||
kBlockTitle,
|
||||
kButton,
|
||||
kLink,
|
||||
kList,
|
||||
kListInput,
|
||||
kListItem,
|
||||
kNavbar,
|
||||
} from 'konsta/vue'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import TimeWheelPicker from '@/components/TimeWheelPicker.vue'
|
||||
import {
|
||||
ALARM_SOUND_IDS,
|
||||
type Alarm,
|
||||
type AlarmDraft,
|
||||
WEEKDAY_IDS,
|
||||
} from '@/utils/alarms'
|
||||
|
||||
const props = defineProps<{ alarm?: Alarm }>()
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
delete: []
|
||||
save: [draft: AlarmDraft]
|
||||
}>()
|
||||
const phone = usePhoneStore()
|
||||
const weekdayKeys = [
|
||||
'sunday',
|
||||
'monday',
|
||||
'tuesday',
|
||||
'wednesday',
|
||||
'thursday',
|
||||
'friday',
|
||||
'saturday',
|
||||
] as const
|
||||
const draft = reactive<AlarmDraft>({
|
||||
note: props.alarm?.note ?? '',
|
||||
sound: props.alarm?.sound ?? 'radar',
|
||||
time: props.alarm?.time ?? '07:00',
|
||||
weekdays: [...(props.alarm?.weekdays ?? [])],
|
||||
})
|
||||
const dangerColors = {
|
||||
tonalBgIos: 'bg-red-500/15 active:bg-red-500/25',
|
||||
tonalTextIos: 'text-red-500',
|
||||
}
|
||||
|
||||
function setNote(event: Event): void {
|
||||
draft.note = (event.target as HTMLInputElement).value.slice(0, 80)
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
emit('save', {
|
||||
note: draft.note,
|
||||
sound: draft.sound,
|
||||
time: draft.time,
|
||||
weekdays: [...draft.weekdays],
|
||||
})
|
||||
}
|
||||
|
||||
function toggleWeekday(weekday: number): void {
|
||||
draft.weekdays = draft.weekdays.includes(weekday)
|
||||
? draft.weekdays.filter((candidate) => candidate !== weekday)
|
||||
: [...draft.weekdays, weekday]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<k-navbar :title="phone.t('Apps.clock.tabs.alarm')">
|
||||
<template #left>
|
||||
<k-link component="button" @click="emit('cancel')">
|
||||
{{ phone.t('Common.cancel') }}
|
||||
</k-link>
|
||||
</template>
|
||||
<template #right>
|
||||
<k-link component="button" @click="save">
|
||||
{{ phone.t('Common.save') }}
|
||||
</k-link>
|
||||
</template>
|
||||
</k-navbar>
|
||||
|
||||
<k-block component="section" class="clock-alarm-editor">
|
||||
<TimeWheelPicker
|
||||
v-model="draft.time"
|
||||
:hours-label="phone.t('Apps.clock.alarm.hours')"
|
||||
:label="phone.t('Apps.clock.alarm.time')"
|
||||
:minutes-label="phone.t('Apps.clock.minutes')"
|
||||
/>
|
||||
|
||||
<k-block-title>{{ phone.t('Apps.clock.alarm.repeat') }}</k-block-title>
|
||||
<k-list strong inset>
|
||||
<k-list-item
|
||||
v-for="weekday in WEEKDAY_IDS"
|
||||
:key="weekday"
|
||||
link
|
||||
:chevron="false"
|
||||
:title="phone.t(`Apps.clock.alarm.days.${weekdayKeys[weekday]}`)"
|
||||
@click="toggleWeekday(weekday)"
|
||||
>
|
||||
<template #after>
|
||||
<Check
|
||||
v-if="draft.weekdays.includes(weekday)"
|
||||
class="w-5 h-5 text-primary"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-list strong inset>
|
||||
<k-list-input
|
||||
:label="phone.t('Apps.clock.alarm.note')"
|
||||
:placeholder="phone.t('Apps.clock.alarm.notePlaceholder')"
|
||||
:value="draft.note"
|
||||
maxlength="80"
|
||||
@input="setNote"
|
||||
/>
|
||||
</k-list>
|
||||
|
||||
<k-block-title>{{ phone.t('Apps.clock.alarm.sound') }}</k-block-title>
|
||||
<k-list strong inset>
|
||||
<k-list-item
|
||||
v-for="sound in ALARM_SOUND_IDS"
|
||||
:key="sound"
|
||||
link
|
||||
:chevron="false"
|
||||
:title="phone.t(`Apps.clock.alarm.sounds.${sound}`)"
|
||||
@click="draft.sound = sound"
|
||||
>
|
||||
<template #after>
|
||||
<Check v-if="draft.sound === sound" class="w-5 h-5 text-primary" />
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-button
|
||||
v-if="alarm"
|
||||
large
|
||||
rounded
|
||||
tonal
|
||||
:colors="dangerColors"
|
||||
class="clock-alarm-delete"
|
||||
@click="emit('delete')"
|
||||
>
|
||||
{{ phone.t('Apps.clock.alarm.delete') }}
|
||||
</k-button>
|
||||
</k-block>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { kNotification } from 'konsta/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { getPhoneApp } from '@/config/apps'
|
||||
import { useNotificationsStore } from '@/stores/notifications'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
const notifications = useNotificationsStore()
|
||||
const phone = usePhoneStore()
|
||||
const icon = computed(() =>
|
||||
notifications.current
|
||||
? getPhoneApp(notifications.current.appId)?.iconImage
|
||||
: undefined,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<k-notification
|
||||
:opened="!!notifications.current"
|
||||
:title="notifications.current?.title"
|
||||
:subtitle="notifications.current?.subtitle"
|
||||
:text="notifications.current?.text"
|
||||
:title-right-text="phone.t('Notifications.now')"
|
||||
button="close"
|
||||
class="phone-notification"
|
||||
@close="notifications.dismissCurrent()"
|
||||
>
|
||||
<template v-if="icon" #icon>
|
||||
<img :src="icon" alt="" class="phone-notification__icon" />
|
||||
</template>
|
||||
<template #button>
|
||||
<span class="sr-only">{{ phone.t('Common.close') }}</span>
|
||||
</template>
|
||||
</k-notification>
|
||||
</template>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
hoursLabel: string
|
||||
label: string
|
||||
minutesLabel: string
|
||||
modelValue: string
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
const ITEM_HEIGHT = 40
|
||||
const hours = Array.from({ length: 24 }, (_, index) => index)
|
||||
const minutes = Array.from({ length: 60 }, (_, index) => index)
|
||||
const initialTime = props.modelValue.split(':').map(Number)
|
||||
const selectedHour = ref(initialTime[0] ?? 0)
|
||||
const selectedMinute = ref(initialTime[1] ?? 0)
|
||||
const hourWheel = ref<HTMLElement | null>(null)
|
||||
const minuteWheel = ref<HTMLElement | null>(null)
|
||||
let updateFrame: number | undefined
|
||||
|
||||
function format(value: number): string {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
function emitTime(): void {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
`${format(selectedHour.value)}:${format(selectedMinute.value)}`,
|
||||
)
|
||||
}
|
||||
|
||||
function updateFromScroll(type: 'hour' | 'minute', element: HTMLElement): void {
|
||||
if (updateFrame) cancelAnimationFrame(updateFrame)
|
||||
updateFrame = requestAnimationFrame(() => {
|
||||
const maximum = type === 'hour' ? hours.length - 1 : minutes.length - 1
|
||||
const value = Math.max(
|
||||
0,
|
||||
Math.min(maximum, Math.round(element.scrollTop / ITEM_HEIGHT)),
|
||||
)
|
||||
if (type === 'hour') selectedHour.value = value
|
||||
else selectedMinute.value = value
|
||||
emitTime()
|
||||
})
|
||||
}
|
||||
|
||||
function select(
|
||||
type: 'hour' | 'minute',
|
||||
value: number,
|
||||
element: HTMLElement | null,
|
||||
): void {
|
||||
if (type === 'hour') selectedHour.value = value
|
||||
else selectedMinute.value = value
|
||||
element?.scrollTo({ behavior: 'smooth', top: value * ITEM_HEIGHT })
|
||||
emitTime()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void nextTick(() => {
|
||||
hourWheel.value?.scrollTo({ top: selectedHour.value * ITEM_HEIGHT })
|
||||
minuteWheel.value?.scrollTo({ top: selectedMinute.value * ITEM_HEIGHT })
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="time-wheel-picker" :aria-label="label">
|
||||
<div class="time-wheel-picker__selection" aria-hidden="true" />
|
||||
<span class="time-wheel-picker__separator" aria-hidden="true">:</span>
|
||||
|
||||
<div
|
||||
ref="hourWheel"
|
||||
class="time-wheel-picker__column"
|
||||
role="listbox"
|
||||
:aria-label="hoursLabel"
|
||||
@scroll.passive="updateFromScroll('hour', $event.currentTarget as HTMLElement)"
|
||||
>
|
||||
<button
|
||||
v-for="hour in hours"
|
||||
:key="hour"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="selectedHour === hour"
|
||||
:class="{ 'time-wheel-picker__value--selected': selectedHour === hour }"
|
||||
class="time-wheel-picker__value"
|
||||
@click="select('hour', hour, hourWheel)"
|
||||
>
|
||||
{{ format(hour) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="minuteWheel"
|
||||
class="time-wheel-picker__column"
|
||||
role="listbox"
|
||||
:aria-label="minutesLabel"
|
||||
@scroll.passive="
|
||||
updateFromScroll('minute', $event.currentTarget as HTMLElement)
|
||||
"
|
||||
>
|
||||
<button
|
||||
v-for="minute in minutes"
|
||||
:key="minute"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="selectedMinute === minute"
|
||||
:class="{
|
||||
'time-wheel-picker__value--selected': selectedMinute === minute,
|
||||
}"
|
||||
class="time-wheel-picker__value"
|
||||
@click="select('minute', minute, minuteWheel)"
|
||||
>
|
||||
{{ format(minute) }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user