mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 17:28:56 +00:00
42ac955052
Add the Companies app, resilient call handling, custom app registry, vendor compatibility, storage policies, frontend bridge, tests, documentation, config, locale, and SQL migrations.
273 lines
7.9 KiB
Vue
273 lines
7.9 KiB
Vue
<script setup lang="ts">
|
|
import { kFab, kGlass } from 'konsta/vue'
|
|
import { Camera, Flashlight, LockKeyhole, X } from 'lucide-vue-next'
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
|
import PhoneStatusIndicators from '@/components/PhoneStatusIndicators.vue'
|
|
import { getPhoneApp } from '@/config/apps'
|
|
import type { PhoneNotification } from '@/stores/notifications'
|
|
import { usePhoneStore } from '@/stores/phone'
|
|
import { nuiCall } from '@/utils/nui'
|
|
import type { PhonePreferencesV1 } from '@/utils/preferences'
|
|
|
|
const props = defineProps<{
|
|
notifications: PhoneNotification[]
|
|
preferences?: PhonePreferencesV1
|
|
preview?: boolean
|
|
}>()
|
|
const emit = defineEmits<{
|
|
camera: []
|
|
clearNotifications: []
|
|
dismissNotification: [id: string]
|
|
openNotification: [notification: PhoneNotification]
|
|
unlock: []
|
|
}>()
|
|
|
|
const phone = usePhoneStore()
|
|
const preferences = computed(() => props.preferences ?? phone.preferences)
|
|
const now = ref(new Date())
|
|
const dragOffset = ref(0)
|
|
const dragging = ref(false)
|
|
const flashlightActive = ref(false)
|
|
const shortcutColors = {
|
|
bgIos: 'bg-ios-light-glass dark:bg-ios-dark-glass',
|
|
activeBgIos: 'active:bg-white/90 dark:active:bg-white/20',
|
|
textIos: 'text-black dark:text-white',
|
|
}
|
|
let pointerStart = 0
|
|
let pointerStartedAt = 0
|
|
let clockTicker: number | undefined
|
|
|
|
const date = computed(() =>
|
|
new Intl.DateTimeFormat(phone.lang, {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
weekday: 'long',
|
|
}).format(now.value),
|
|
)
|
|
const time = computed(() =>
|
|
new Intl.DateTimeFormat(phone.lang, {
|
|
hour: '2-digit',
|
|
hourCycle: 'h23',
|
|
minute: '2-digit',
|
|
})
|
|
.formatToParts(now.value)
|
|
.filter((part) => part.type !== 'dayPeriod')
|
|
.map((part) => part.value)
|
|
.join('')
|
|
.trim(),
|
|
)
|
|
const dragStyle = computed(() => ({
|
|
'--lock-drag': `${dragOffset.value}px`,
|
|
}))
|
|
const flashlightShortcutColors = computed(() =>
|
|
flashlightActive.value
|
|
? {
|
|
...shortcutColors,
|
|
bgIos: 'bg-white',
|
|
textIos: 'text-purple-500',
|
|
}
|
|
: shortcutColors,
|
|
)
|
|
|
|
function onPointerDown(event: PointerEvent): void {
|
|
if (props.preview) return
|
|
if (
|
|
(event.target as HTMLElement).closest('button, .lock-screen__notifications')
|
|
)
|
|
return
|
|
pointerStart = event.clientY
|
|
pointerStartedAt = Date.now()
|
|
dragging.value = true
|
|
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
|
|
}
|
|
|
|
function openNotification(notification: PhoneNotification): void {
|
|
if (notification.route) emit('openNotification', notification)
|
|
}
|
|
|
|
function onPointerMove(event: PointerEvent): void {
|
|
if (!dragging.value) return
|
|
dragOffset.value = Math.min(0, event.clientY - pointerStart)
|
|
}
|
|
|
|
function finishPointer(event: PointerEvent): void {
|
|
if (!dragging.value) return
|
|
const distance = event.clientY - pointerStart
|
|
const elapsed = Math.max(1, Date.now() - pointerStartedAt)
|
|
const velocity = Math.abs(distance) / elapsed
|
|
dragging.value = false
|
|
|
|
if (distance < -74 || (distance < -22 && velocity > 0.55)) {
|
|
emit('unlock')
|
|
return
|
|
}
|
|
|
|
dragOffset.value = 0
|
|
}
|
|
|
|
function unlockWithKeyboard(event: KeyboardEvent): void {
|
|
if (props.preview) return
|
|
if (event.key === 'Enter' || event.key === ' ') emit('unlock')
|
|
}
|
|
|
|
function unlockFromWallpaper(event: MouseEvent): void {
|
|
if (props.preview) return
|
|
if (
|
|
(event.target as HTMLElement).closest(
|
|
'button, [role="link"], .lock-screen__notifications',
|
|
)
|
|
)
|
|
return
|
|
emit('unlock')
|
|
}
|
|
|
|
async function toggleFlashlight(): Promise<void> {
|
|
const enabled = !flashlightActive.value
|
|
flashlightActive.value = enabled
|
|
const response = await nuiCall('camera:setFlash', { enabled })
|
|
if (!response.success) flashlightActive.value = !enabled
|
|
}
|
|
|
|
onMounted(() => {
|
|
clockTicker = window.setInterval(() => {
|
|
now.value = new Date()
|
|
}, 15_000)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (clockTicker !== undefined) window.clearInterval(clockTicker)
|
|
if (flashlightActive.value)
|
|
void nuiCall('camera:setFlash', { enabled: false })
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
class="lock-screen"
|
|
:class="[
|
|
`wallpaper--${preferences.settings.wallpaper}`,
|
|
{
|
|
'lock-screen--dragging': dragging,
|
|
'lock-screen--preview': preview,
|
|
},
|
|
]"
|
|
:style="dragStyle"
|
|
:aria-label="phone.t('LockScreen.label')"
|
|
:tabindex="preview ? -1 : 0"
|
|
@keydown="unlockWithKeyboard"
|
|
@pointerdown="onPointerDown"
|
|
@pointermove="onPointerMove"
|
|
@pointerup="finishPointer"
|
|
@pointercancel="finishPointer"
|
|
@click="unlockFromWallpaper"
|
|
>
|
|
<div class="lock-screen__shade" aria-hidden="true"></div>
|
|
|
|
<header class="lock-screen__status">
|
|
<time class="lock-screen__status-time">{{ time }}</time>
|
|
<PhoneStatusIndicators class="lock-screen__indicators" />
|
|
</header>
|
|
|
|
<LockKeyhole
|
|
class="lock-screen__lock"
|
|
:size="14"
|
|
:stroke-width="1.8"
|
|
aria-hidden="true"
|
|
/>
|
|
<div class="lock-screen__content">
|
|
<time class="lock-screen__date">{{ date }}</time>
|
|
<time class="lock-screen__time">{{ time }}</time>
|
|
</div>
|
|
|
|
<section
|
|
v-if="props.notifications.length"
|
|
class="lock-screen__notifications"
|
|
aria-live="polite"
|
|
>
|
|
<button
|
|
v-if="!preview && props.notifications.length > 1"
|
|
class="lock-screen__notifications-clear"
|
|
type="button"
|
|
@click.stop="emit('clearNotifications')"
|
|
>
|
|
{{ phone.t('Notifications.clearAll') }}
|
|
</button>
|
|
<k-glass
|
|
v-for="notification in props.notifications"
|
|
:key="notification.id"
|
|
class="lock-screen__notification"
|
|
:class="{ 'is-actionable': !!notification.route }"
|
|
:highlight="false"
|
|
:role="notification.route ? 'button' : undefined"
|
|
:tabindex="notification.route ? 0 : undefined"
|
|
:aria-label="
|
|
notification.route ? phone.t('Notifications.open') : undefined
|
|
"
|
|
@click="openNotification(notification)"
|
|
@keydown.enter.prevent="openNotification(notification)"
|
|
@keydown.space.prevent="openNotification(notification)"
|
|
>
|
|
<img
|
|
v-if="getPhoneApp(notification.appId)?.iconImage"
|
|
:src="getPhoneApp(notification.appId)?.iconImage"
|
|
alt=""
|
|
class="lock-screen__notification-icon"
|
|
/>
|
|
<div class="lock-screen__notification-copy">
|
|
<div class="lock-screen__notification-heading">
|
|
<strong>{{ notification.title }}</strong>
|
|
<span>{{ phone.t('Notifications.now') }}</span>
|
|
</div>
|
|
<small v-if="notification.subtitle">{{
|
|
notification.subtitle
|
|
}}</small>
|
|
<p>{{ notification.text }}</p>
|
|
</div>
|
|
<button
|
|
class="lock-screen__notification-close"
|
|
type="button"
|
|
:aria-label="phone.t('Common.close')"
|
|
@click.stop="emit('dismissNotification', notification.id)"
|
|
>
|
|
<X :size="14" aria-hidden="true" />
|
|
</button>
|
|
</k-glass>
|
|
</section>
|
|
|
|
<div v-if="!preview" class="lock-screen__footer">
|
|
<nav class="lock-screen__shortcuts">
|
|
<k-fab
|
|
component="button"
|
|
type="button"
|
|
class="lock-screen__shortcut"
|
|
:colors="flashlightShortcutColors"
|
|
:aria-label="phone.t('LockScreen.flashlight')"
|
|
@click="toggleFlashlight"
|
|
>
|
|
<template #icon>
|
|
<Flashlight :stroke-width="1.4" aria-hidden="true" />
|
|
</template>
|
|
</k-fab>
|
|
<k-fab
|
|
component="button"
|
|
type="button"
|
|
class="lock-screen__shortcut"
|
|
:colors="shortcutColors"
|
|
:aria-label="phone.t('LockScreen.camera')"
|
|
@click="emit('camera')"
|
|
>
|
|
<template #icon>
|
|
<Camera :stroke-width="1.4" aria-hidden="true" />
|
|
</template>
|
|
</k-fab>
|
|
</nav>
|
|
|
|
<button class="lock-screen__swipe" type="button" @click="emit('unlock')">
|
|
<span class="lock-screen__swipe-chevron" aria-hidden="true"></span>
|
|
{{ phone.t('LockScreen.swipeUp') }}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|