// Gesture Controller - Android 14 compliant gesture handling class GestureController { constructor() { this.minSwipeDistance = 60; this.edgeSwipeWidth = 40; this.gestureArea = document.documentElement; this.isTracking = false; this.startY = 0; this.startX = 0; this.currentGesture = null; this.vibrationEnabled = true; this.init(); } init() { this.setupTouchHandlers(); this.setupAndroidGestures(); this.listenForSettings(); } setupTouchHandlers() { // Use passive listeners for performance document.addEventListener('touchstart', (e) => this.onTouchStart(e), { passive: true }); document.addEventListener('touchmove', (e) => this.onTouchMove(e), { passive: true }); document.addEventListener('touchend', (e) => this.onTouchEnd(e), { passive: true }); document.addEventListener('touchcancel', (e) => this.onTouchCancel(e), { passive: true }); // Prevent default only for specific gestures document.addEventListener('touchmove', (e) => { if (this.isTracking && this.shouldPreventScroll()) { e.preventDefault(); } }, { passive: false }); } setupAndroidGestures() { // Register with Android system if available if (window.Android?.GestureManager?.registerCallback) { window.Android.GestureManager.registerCallback((gesture) => { this.handleAndroidGesture(gesture); }); } } listenForSettings() { window.addEventListener('settings-updated', (e) => { if (e.detail.settings) { const settings = e.detail.settings; this.vibrationEnabled = settings['haptic_feedback'] !== '0'; } }); } onTouchStart(e) { const touch = e.touches[0]; this.startX = touch.clientX; this.startY = touch.clientY; this.startTime = Date.now(); this.isTracking = true; this.currentGesture = null; // Detect gesture zone const screenHeight = window.innerHeight; const screenWidth = window.innerWidth; this.gestureType = this.detectGestureZone(this.startX, this.startY, screenWidth, screenHeight); // Send to worker for processing if (window.workerBridge) { window.workerBridge.processGesture('TOUCH_START', { x: this.startX, y: this.startY, time: this.startTime }); } } onTouchMove(e) { if (!this.isTracking) return; const touch = e.touches[0]; const deltaX = touch.clientX - this.startX; const deltaY = touch.clientY - this.startY; // Send to worker if (window.workerBridge) { window.workerBridge.processGesture('TOUCH_MOVE', { x: touch.clientX, y: touch.clientY, time: Date.now() }); } // Live gesture detection for UI feedback this.updateGesturePreview(deltaX, deltaY); } onTouchEnd(e) { if (!this.isTracking) return; const touch = e.changedTouches[0]; const deltaX = touch.clientX - this.startX; const deltaY = touch.clientY - this.startY; const deltaTime = Date.now() - this.startTime; this.isTracking = false; // Send to worker if (window.workerBridge) { window.workerBridge.processGesture('TOUCH_END', { x: touch.clientX, y: touch.clientY, time: Date.now() }); } // Local gesture detection for immediate response const gesture = this.detectGesture(deltaX, deltaY, deltaTime); if (gesture) { this.executeGesture(gesture); } this.hideGesturePreview(); } onTouchCancel(e) { this.isTracking = false; this.hideGesturePreview(); } detectGestureZone(x, y, width, height) { // Android 14 gesture zones if (y < 50) { if (x < width * 0.4) return 'STATUS_NOTIFICATIONS'; if (x > width * 0.6) return 'STATUS_QUICK_SETTINGS'; return 'STATUS_CLOCK'; } if (y > height - 20 && x > width * 0.2 && x < width * 0.8) { return 'HOME_INDICATOR'; } if (x < this.edgeSwipeWidth) return 'EDGE_BACK'; if (x > width - this.edgeSwipeWidth) return 'EDGE_FORWARD'; return 'CONTENT'; } detectGesture(deltaX, deltaY, deltaTime) { const absX = Math.abs(deltaX); const absY = Math.abs(deltaY); const velocityX = absX / deltaTime; const velocityY = absY / deltaTime; const minVelocity = 0.3; // px/ms // Home gesture (bottom swipe up) if (this.gestureType === 'HOME_INDICATOR' && deltaY < -this.minSwipeDistance) { return { type: 'HOME', intensity: Math.min(absY / 200, 1) }; } // Back gesture (edge swipe) if (this.gestureType === 'EDGE_BACK' && deltaX > this.minSwipeDistance) { return { type: 'BACK', edge: 'left' }; } // Quick settings (top-right swipe down) if (this.gestureType === 'STATUS_QUICK_SETTINGS' && deltaY > this.minSwipeDistance) { return { type: 'QUICK_SETTINGS' }; } // Notifications (top-left swipe down) if (this.gestureType === 'STATUS_NOTIFICATIONS' && deltaY > this.minSwipeDistance) { return { type: 'NOTIFICATIONS' }; } // App drawer (home screen swipe up) if (absY > absX && deltaY < -this.minSwipeDistance && velocityY > minVelocity) { if (document.getElementById('main-screen')?.contains(document.elementFromPoint(this.startX, this.startY))) { return { type: 'APP_DRAWER' }; } } // General swipes if (absX > absY && absX > this.minSwipeDistance && velocityX > minVelocity) { return { type: deltaX > 0 ? 'SWIPE_RIGHT' : 'SWIPE_LEFT' }; } return null; } executeGesture(gesture) { if (this.vibrationEnabled) { this.hapticFeedback(gesture.type); } switch (gesture.type) { case 'HOME': this.goHome(); break; case 'BACK': this.goBack(); break; case 'QUICK_SETTINGS': case 'APP_DRAWER': this.togglePanel('settings-panel'); break; case 'NOTIFICATIONS': this.togglePanel('notification-shade'); break; } // Dispatch event for components window.dispatchEvent(new CustomEvent('gesture-executed', { detail: gesture })); } goHome() { // Close all panels ['server-panel', 'settings-panel', 'notification-shade', 'app-drawer'].forEach(id => { const el = document.getElementById(id); if (el) { el.classList.add('-translate-y-full', '-translate-x-full', 'translate-y-full'); el.classList.remove('translate-y-0', 'translate-x-0', 'translate-y-full'); } }); // Reset main screen const main = document.getElementById('main-screen'); if (main) { main.style.transform = ''; } } goBack() { // Close topmost panel or go back in history const panels = ['notification-shade', 'settings-panel', 'server-panel', 'app-drawer']; for (const id of panels) { const el = document.getElementById(id); if (el && this.isPanelOpen(el)) { this.closePanel(el); return; } } // Fallback to browser back if (history.length > 1) { history.back(); } } togglePanel(id) { const el = document.getElementById(id); if (!el) return; if (this.isPanelOpen(el)) { this.closePanel(el); } else { this.openPanel(el); } } openPanel(el) { const id = el.id; if (id === 'notification-shade') { el.classList.remove('-translate-y-full'); el.classList.add('translate-y-0'); } else if (id === 'server-panel') { el.classList.remove('-translate-x-full'); el.classList.add('translate-x-0'); } else { el.classList.remove('translate-y-full'); el.classList.add('translate-y-0'); } } closePanel(el) { const id = el.id; if (id === 'notification-shade') { el.classList.add('-translate-y-full'); el.classList.remove('translate-y-0'); } else if (id === 'server-panel') { el.classList.add('-translate-x-full'); el.classList.remove('translate-x-0'); } else { el.classList.add('translate-y-full'); el.classList.remove('translate-y-0'); } } isPanelOpen(el) { return !el.classList.contains('-translate-y-full') && !el.classList.contains('-translate-x-full') && !el.classList.contains('translate-y-full'); } hapticFeedback(type) { const patterns = { 'HOME': [0, 20, 10, 20], 'BACK': [0, 15], 'QUICK_SETTINGS': [0, 10, 40, 10], 'NOTIFICATIONS': [0, 10, 40, 10], 'default': [0, 20] }; if (navigator.vibrate) { navigator.vibrate(patterns[type] || patterns.default); } // Also notify Android if (window.Android?.Haptic?.perform) { window.Android.Haptic.perform(type.toLowerCase()); } } updateGesturePreview(deltaX, deltaY) { // Visual feedback during gesture const indicator = document.getElementById('gesture-indicator'); if (!indicator) return; const progress = Math.min(Math.abs(deltaY) / 200, 1); indicator.style.opacity = progress; indicator.style.transform = `translateY(${Math.min(deltaY * 0.3, 50)}px)`; } hideGesturePreview() { const indicator = document.getElementById('gesture-indicator'); if (indicator) { indicator.style.opacity = '0'; } } shouldPreventScroll() { return ['HOME_INDICATOR', 'STATUS_NOTIFICATIONS', 'STATUS_QUICK_SETTINGS'].includes(this.gestureType); } handleAndroidGesture(gesture) { // Handle gestures from Android system this.executeGesture({ type: gesture.type }); } } // Initialize when DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { window.gestureController = new GestureController(); }); } else { window.gestureController = new GestureController(); }