From bc308f0f69a16d0d9e49d7fa0fc05ccfb7918ecc Mon Sep 17 00:00:00 2001 From: "smx.pusha" <139338836+smxpusha@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:49:47 +0200 Subject: [PATCH] ADD - persist lock screen notifications --- frontend/src/App.vue | 44 +++++- frontend/src/assets/main.css | 146 ++++++++++++++++- .../components/NotificationPhonePreview.vue | 12 +- frontend/src/components/PhoneLockScreen.vue | 90 +++++++++-- frontend/src/components/PhoneStatusBar.vue | 23 ++- frontend/src/stores/notifications.test.ts | 132 +++++++++++++++- frontend/src/stores/notifications.ts | 147 +++++++++++++++++- sky_phone/config/config.lua | 5 + sky_phone/config/locales/en.lua | 2 +- sky_phone/fxmanifest.lua | 1 + sky_phone/source/client/main.lua | 5 + sky_phone/source/server/debug.lua | 78 ++++++++++ sky_phone/source/server/phone.lua | 54 +++++++ 13 files changed, 704 insertions(+), 35 deletions(-) create mode 100644 sky_phone/source/server/debug.lua diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a8677ee..e4ec154 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -82,6 +82,10 @@ type SimPickerPayload = { number: string } +type NotificationEventData = Omit & { + device?: PhoneNotificationDevicePayload +} + type MailEventData = { counts?: MailCounts device?: PhoneNotificationDevicePayload @@ -265,6 +269,13 @@ function getViewportScale(): number { function hydratePhone(payload: PhoneOpenPayload): void { phone.open(payload) + if (payload.device?.imei) { + notifications.hydrate( + payload.device.data.notifications?.payload, + payload.device.imei, + ) + notifications.hideDevicePreview(payload.device.imei) + } account.hydrate(payload.account ?? null) notes.hydrate(payload.notes ?? []) clock.hydrate(payload.device?.data.alarms?.payload) @@ -336,7 +347,20 @@ function onMessage(event: MessageEvent): void { } else if (event.data?.type === 'app:resume') { activitySuspended.value = false } else if (event.data?.type === 'notification:show' && event.data.data) { - notifications.show(event.data.data as PhoneNotificationInput) + const data = event.data.data as NotificationEventData + const { device, ...input } = data + const notification: PhoneNotificationInput = input + if ( + device && + (!phone.isOpen || device.imei !== phone.device?.imei) + ) { + notification.device = { + imei: device.imei, + name: device.name, + preferences: parsePhonePreferences(device.settings ?? null), + } + } + notifications.show(notification) } else if (event.data?.type === 'mail:changed' && event.data.data) { const data = event.data.data as MailEventData if (data.counts) mail.setCounts(data.counts) @@ -749,6 +773,17 @@ function toggleControlCenter(): void { controlCenterOpened.value = !controlCenterOpened.value } +function lockPhone(): void { + if (!phone.isOpen || isLocked.value) return + controlCenterOpened.value = false + isUnlocking.value = false + passcodeVisible.value = false + passcodeBusy.value = false + passcodeError.value = '' + pendingUnlockRoute.value = null + isLocked.value = true +} + function unlockCamera(): void { if (phone.security.enabled) { pendingUnlockRoute.value = '/apps/camera' @@ -975,7 +1010,9 @@ onBeforeUnmount(() => { @@ -995,7 +1032,10 @@ onBeforeUnmount(() => { @@ -1020,7 +1060,7 @@ onBeforeUnmount(() => { /> diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 3e7c51e..6006bb2 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -268,10 +268,6 @@ button { transform: translateY(calc(100% - 190px)); transform-origin: right bottom; } -.notification-phone-background { - position: absolute; - inset: 0; -} .phone-device { position: relative; width: 390px; @@ -458,6 +454,19 @@ button { width: calc(50% - 70px); text-align: center; } +.phone-status-bar__time-button { + top: 0; + height: 48px; + border: 0; + padding: 17px 0 0; + color: inherit; + background: transparent; + font: inherit; + letter-spacing: inherit; + line-height: inherit; + cursor: pointer; + pointer-events: auto; +} .phone-status-bar__indicators { position: absolute; top: 0; @@ -569,7 +578,7 @@ button { left: 30px; display: flex; align-items: center; - justify-content: flex-end; + justify-content: space-between; min-height: 26px; filter: drop-shadow(0 1px 2px #0009); } @@ -615,9 +624,123 @@ button { 0 2px 16px #0006; filter: blur(0.24px); } +.lock-screen__status-time { + color: #fff; + font-size: 14px; + font-weight: 650; + font-variant-numeric: tabular-nums; +} +.lock-screen__notifications { + position: absolute; + z-index: 2; + top: 285px; + right: 18px; + bottom: 142px; + left: 18px; + display: grid; + align-content: start; + gap: 8px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: none; + touch-action: pan-y; + -webkit-overflow-scrolling: touch; +} +.lock-screen__notifications::-webkit-scrollbar { + display: none; +} +.lock-screen__notifications-clear { + position: sticky; + z-index: 5; + top: 0; + justify-self: end; + border: 0; + border-radius: 999px; + padding: 5px 10px; + color: #fff; + background: #1c1c20b8; + box-shadow: 0 2px 10px #0004; + font-size: 11px; + font-weight: 600; + cursor: pointer; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); +} +.lock-screen__notification { + position: relative; + display: grid; + grid-template-columns: 42px minmax(0, 1fr); + gap: 10px; + min-height: 72px; + border-radius: 18px; + padding: 11px 38px 11px 11px; + color: #fff; + background-color: rgb(28 28 32 / 72%); + text-shadow: 0 1px 3px #0009; +} +.lock-screen__notification-icon { + width: 42px; + height: 42px; + border-radius: 10px; + object-fit: contain; +} +.lock-screen__notification-copy { + min-width: 0; +} +.lock-screen__notification-heading { + display: flex; + min-width: 0; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} +.lock-screen__notification-heading strong { + overflow: hidden; + font-size: 13px; + line-height: 17px; + text-overflow: ellipsis; + white-space: nowrap; +} +.lock-screen__notification-heading span, +.lock-screen__notification-copy small { + color: #ffffffa6; + font-size: 10px; + line-height: 14px; +} +.lock-screen__notification-copy small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.lock-screen__notification-copy p { + display: -webkit-box; + overflow: hidden; + margin: 2px 0 0; + font-size: 12px; + line-height: 15px; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} +.lock-screen__notification-close { + position: absolute; + top: 8px; + right: 8px; + display: grid; + width: 24px; + height: 24px; + place-items: center; + border: 0; + border-radius: 50%; + padding: 0; + color: #fff; + background: #0007; + cursor: pointer; +} .lock-screen__footer { position: absolute; - z-index: 1; + z-index: 3; right: 0; bottom: 25px; left: 0; @@ -656,6 +779,16 @@ button { transform: rotate(45deg); animation: lock-chevron 1.8s ease-in-out infinite; } +.lock-screen.lock-screen-enter-active { + transform-origin: top left; + transition: + transform 0.44s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.28s ease-out; +} +.lock-screen.lock-screen-enter-from { + transform: translate3d(-28px, -22px, 0) scale(0.84); + opacity: 0; +} .lock-screen-leave-active { transition: transform 0.64s cubic-bezier(0.32, 0.72, 0, 1), @@ -694,6 +827,7 @@ button { .lock-screen__swipe-chevron { animation: none; } + .lock-screen.lock-screen-enter-active, .lock-screen-leave-active { transition-duration: 0.18s; } diff --git a/frontend/src/components/NotificationPhonePreview.vue b/frontend/src/components/NotificationPhonePreview.vue index 2033906..d5e8e1a 100644 --- a/frontend/src/components/NotificationPhonePreview.vue +++ b/frontend/src/components/NotificationPhonePreview.vue @@ -2,8 +2,8 @@ import { kApp } from 'konsta/vue' import { computed, type CSSProperties } from 'vue' +import PhoneLockScreen from '@/components/PhoneLockScreen.vue' import PhoneNotifications from '@/components/PhoneNotifications.vue' -import PhoneStatusBar from '@/components/PhoneStatusBar.vue' import { PHONE_FRAME_IMAGES } from '@/config/appearance' import type { PhoneNotification } from '@/stores/notifications' import { usePhoneStore } from '@/stores/phone' @@ -61,11 +61,11 @@ const wrapperStyle = computed(() => ({ zoom: props.zoom })) [`phone-app--${preferences.settings.graphicsMode}`]: true, }" > -
- + -import { kFab } from 'konsta/vue' -import { Camera, Flashlight, LockKeyhole } from 'lucide-vue-next' +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] unlock: [] }>() const phone = usePhoneStore() +const preferences = computed(() => props.preferences ?? phone.preferences) const now = ref(new Date()) const dragOffset = ref(0) const dragging = ref(false) @@ -59,7 +70,13 @@ const flashlightShortcutColors = computed(() => ) function onPointerDown(event: PointerEvent): void { - if ((event.target as HTMLElement).closest('button')) return + if (props.preview) return + if ( + (event.target as HTMLElement).closest( + 'button, .lock-screen__notifications', + ) + ) + return pointerStart = event.clientY pointerStartedAt = Date.now() dragging.value = true @@ -87,11 +104,18 @@ function finishPointer(event: PointerEvent): void { } function unlockWithKeyboard(event: KeyboardEvent): void { + if (props.preview) return if (event.key === 'Enter' || event.key === ' ') emit('unlock') } function unlockFromWallpaper(event: MouseEvent): void { - if ((event.target as HTMLElement).closest('button, [role="link"]')) return + if (props.preview) return + if ( + (event.target as HTMLElement).closest( + 'button, [role="link"], .lock-screen__notifications', + ) + ) + return emit('unlock') } @@ -119,12 +143,15 @@ onBeforeUnmount(() => {
{
+
+