From b54839ee7442e83c2b372ecac7b3e18520e01d23 Mon Sep 17 00:00:00 2001 From: DerEchteAlec Date: Wed, 12 Aug 2026 18:59:33 +0200 Subject: [PATCH] FIX - make phone input and overlays CEF safe --- frontend/src/components/AppIcon.vue | 62 +++++++--- frontend/src/components/DarkChatSelect.vue | 5 +- frontend/src/components/EasyShareSheet.vue | 30 +++-- frontend/src/components/SpringboardWidget.vue | 62 +++++++--- .../src/components/SpringboardWidgetGrid.vue | 3 + .../components/citymarkt/CityMarktSelect.vue | 3 + frontend/src/utils/homeLayout.test.ts | 10 ++ frontend/src/utils/homeLayout.ts | 30 +++++ frontend/src/utils/keyboard.test.ts | 98 +++++++++++++++ frontend/src/utils/keyboard.ts | 47 ++++++++ frontend/src/utils/musicEscape.test.ts | 38 ++++++ frontend/src/utils/musicEscape.ts | 22 ++++ frontend/src/utils/widgetLayout.test.ts | 16 +++ frontend/src/utils/widgetLayout.ts | 27 +++++ frontend/src/views/SpringboardView.vue | 30 +++++ frontend/src/views/apps/BankingApp.vue | 113 +++++++++++++----- frontend/src/views/apps/CompaniesApp.vue | 3 +- frontend/src/views/apps/CrewLinkApp.vue | 37 +++++- frontend/src/views/apps/DarkChatApp.vue | 5 +- frontend/src/views/apps/FeatherApp.vue | 101 +++++++++++++++- frontend/src/views/apps/FlareApp.vue | 20 +++- frontend/src/views/apps/MapApp.vue | 3 +- frontend/src/views/apps/MessagesApp.vue | 3 +- frontend/src/views/apps/MusicApp.vue | 74 ++++++++---- 24 files changed, 730 insertions(+), 112 deletions(-) create mode 100644 frontend/src/utils/keyboard.test.ts create mode 100644 frontend/src/utils/keyboard.ts create mode 100644 frontend/src/utils/musicEscape.test.ts create mode 100644 frontend/src/utils/musicEscape.ts diff --git a/frontend/src/components/AppIcon.vue b/frontend/src/components/AppIcon.vue index 17fb331..8e2da12 100644 --- a/frontend/src/components/AppIcon.vue +++ b/frontend/src/components/AppIcon.vue @@ -12,6 +12,10 @@ import { useMarketplaceStore } from '@/stores/marketplace' import { useDarkChatStore } from '@/stores/darkchat' import { usePhoneStore } from '@/stores/phone' import type { PhoneAppDefinition } from '@/types/apps' +import { + reorderDirectionFromKeyboard, + type ReorderDirection, +} from '@/utils/keyboard' const props = withDefaults( defineProps<{ @@ -33,6 +37,7 @@ const emit = defineEmits<{ dragstart: [event: PointerEvent] edit: [] remove: [] + reorder: [direction: ReorderDirection] }>() const phone = usePhoneStore() @@ -65,6 +70,8 @@ const suppressClick = ref(false) let holdTimer: number | undefined let calendarTimer: number | undefined let pointerStart = { x: 0, y: 0 } +let pointerTarget: HTMLElement | null = null +let pointerId: number | null = null watch( () => props.app.iconImage, @@ -135,6 +142,9 @@ function clearHold(): void { function onPointerDown(event: PointerEvent): void { if (props.compact || event.button !== 0) return + pointerTarget = event.currentTarget as HTMLElement + pointerId = event.pointerId + pointerTarget.setPointerCapture(pointerId) pointerStart = { x: event.clientX, y: event.clientY } clearHold() if (props.editMode) { @@ -173,35 +183,48 @@ function beginPointerDrag(event: PointerEvent): void { .closest('.springboard-page') ?.getBoundingClientRect().width ?? 0 isDragging.value = true - window.addEventListener('pointermove', onPointerMove) - window.addEventListener('pointerup', onPointerUp) - window.addEventListener('pointercancel', cancelPointerDrag) emit('dragstart', event) } function onPointerUp(event: PointerEvent): void { clearHold() - if (!isDragging.value) return - suppressClick.value = true - emit('dragend', event) - isDragging.value = false - dragOffset.value = { x: 0, y: 0 } - removeDragListeners() + if (isDragging.value) { + suppressClick.value = true + emit('dragend', event) + isDragging.value = false + dragOffset.value = { x: 0, y: 0 } + } + releasePointerCapture() } function cancelPointerDrag(): void { clearHold() - if (!isDragging.value) return + const wasDragging = isDragging.value isDragging.value = false dragOffset.value = { x: 0, y: 0 } - removeDragListeners() - emit('dragcancel') + releasePointerCapture() + if (wasDragging) emit('dragcancel') } -function removeDragListeners(): void { - window.removeEventListener('pointermove', onPointerMove) - window.removeEventListener('pointerup', onPointerUp) - window.removeEventListener('pointercancel', cancelPointerDrag) +function releasePointerCapture(): void { + if ( + pointerTarget && + pointerId !== null && + pointerTarget.hasPointerCapture(pointerId) + ) { + pointerTarget.releasePointerCapture(pointerId) + } + pointerTarget = null + pointerId = null +} + +function onKeydown(event: KeyboardEvent): void { + if (!props.editMode) return + const direction = reorderDirectionFromKeyboard(event) + if (!direction) return + event.preventDefault() + event.stopPropagation() + emit('reorder', direction) } onMounted(() => { @@ -214,7 +237,7 @@ onMounted(() => { onBeforeUnmount(() => { clearHold() if (calendarTimer !== undefined) window.clearInterval(calendarTimer) - removeDragListeners() + releasePointerCapture() }) @@ -234,10 +257,15 @@ onBeforeUnmount(() => { type="button" :aria-label="getPhoneAppLabel(app, phone.t)" :aria-disabled="!app.route" + :aria-keyshortcuts=" + editMode ? 'ArrowLeft ArrowRight ArrowUp ArrowDown' : undefined + " @click="launch" @contextmenu.prevent + @keydown="onKeydown" @pointercancel="cancelPointerDrag" @pointerdown="onPointerDown" + @lostpointercapture="cancelPointerDrag" @pointerleave="isDragging || clearHold()" @pointermove="onPointerMove" @pointerup="onPointerUp" diff --git a/frontend/src/components/DarkChatSelect.vue b/frontend/src/components/DarkChatSelect.vue index 804ae57..baa33d1 100644 --- a/frontend/src/components/DarkChatSelect.vue +++ b/frontend/src/components/DarkChatSelect.vue @@ -35,7 +35,10 @@ function closeFromOutside(event: PointerEvent): void { } function closeFromEscape(event: KeyboardEvent): void { - if (event.key === 'Escape') opened.value = false + if (event.key !== 'Escape' || !opened.value) return + event.preventDefault() + event.stopPropagation() + opened.value = false } onMounted(() => { diff --git a/frontend/src/components/EasyShareSheet.vue b/frontend/src/components/EasyShareSheet.vue index 9e83d30..48da343 100644 --- a/frontend/src/components/EasyShareSheet.vue +++ b/frontend/src/components/EasyShareSheet.vue @@ -9,7 +9,7 @@ import { UserRound, X, } from 'lucide-vue-next' -import { computed, ref } from 'vue' +import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import { getPhoneApp, getPhoneAppLabel } from '@/config/apps' @@ -30,6 +30,7 @@ import { easyShareDestinationAppIds, openEasySharePayload, } from '@/utils/easyshare' +import { consumeEscape } from '@/utils/keyboard' const phone = usePhoneStore() const appStore = useAppStoreStore() @@ -113,11 +114,8 @@ const shareApps = computed(() => return app ? [{ app, id }] : [] }), ) -const sheetStyle = computed(() => ({ - transform: easyShare.opened - ? `translateY(calc(-100% + ${dragOffset.value}px))` - : undefined, - transitionDuration: dragging.value ? '0ms' : undefined, +const hostStyle = computed(() => ({ + '--easyshare-drag-offset': `${dragOffset.value}px`, })) function label(key: string, params?: Record): string { @@ -155,6 +153,11 @@ function close(): void { easyShare.close() } +function onKeydown(event: KeyboardEvent): void { + if (!easyShare.opened || !consumeEscape(event)) return + close() +} + function beginDrag(event: PointerEvent): void { if (!easyShare.opened || event.button !== 0) return dragPointerId = event.pointerId @@ -240,17 +243,22 @@ async function openTransfer(transfer: EasyShareTransfer): Promise { close() await openEasySharePayload(router, transfer.payload) } + +onMounted(() => window.addEventListener('keydown', onKeydown, true)) +onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true)) diff --git a/frontend/src/views/apps/FlareApp.vue b/frontend/src/views/apps/FlareApp.vue index 6eda1a4..f7dbca5 100644 --- a/frontend/src/views/apps/FlareApp.vue +++ b/frontend/src/views/apps/FlareApp.vue @@ -85,6 +85,7 @@ import type { PhoneMedia } from '@/types/media' import type { EasySharePayload } from '@/types/easyshare' import type { GifSearchResult, SmsAttachmentType } from '@/types/messages' import { parseDatabaseDate, type DatabaseDateValue } from '@/utils/date' +import { handleEnterAction } from '@/utils/keyboard' type FlareTab = 'discover' | 'explore' | 'likes' | 'matches' | 'profile' type ExploreMode = 'all' | 'dates' | 'friends' | 'longTerm' @@ -1145,7 +1146,7 @@ onBeforeUnmount(() => { :value="draft" :disabled="flare.sending" @input="draft = eventValue($event)" - @keydown.enter.exact.prevent="sendMessage" + @keydown.enter.exact="handleEnterAction($event, sendMessage)" >