diff --git a/README.md b/README.md index 93b27ce..09f0c03 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ A modern, production-ready template for building UserScripts using TypeScript an • 🎨 **DOM Utilities:** Helper functions for element manipulation and waiting • 🔒 **Error Handling:** Comprehensive error boundary system • ⚡ **Event System:** Type-safe event emitter for module communication +• 📱 **Mobile Support:** Touch-optimized interface with mobile browser detection +• 🤏 **Touch Gestures:** Built-in touch event handling and gesture recognition +• 📲 **Responsive Design:** Mobile-first CSS with safe area support for notched devices ## Installation @@ -202,6 +205,39 @@ export class MyModule extends EventEmitter { } ``` +### Mobile Utilities + +Mobile-specific functionality for touch-enabled devices: + +```typescript +import { MobileUtils } from '@/utils/mobile'; + +// Detect mobile browser and capabilities +const detection = MobileUtils.detect(); +console.log('Is Mobile:', detection.isMobile); +console.log('Has Touch:', detection.hasTouch); +console.log('Browser:', detection.browser); + +// Add mobile-optimized styles +if (detection.isMobile) { + MobileUtils.addMobileStyles(); +} + +// Unified touch/mouse event handling +MobileUtils.addUnifiedEventListener(element, 'start', event => { + const position = MobileUtils.getEventPosition(event); + console.log('Touch/click at:', position); +}); + +// Create mobile-friendly buttons +const button = mobileModule.createMobileButton('Action', () => { + console.log('Button pressed'); +}); + +// Orientation detection +console.log('Portrait mode:', MobileUtils.isPortrait()); +``` + ## UserScript Compatibility • **Tampermonkey:** Full support with all GM\_\* APIs @@ -209,6 +245,28 @@ export class MyModule extends EventEmitter { • **Violentmonkey:** Full compatibility • **Safari:** Works with userscript managers +### Mobile Browser Support + +**Android:** + +- **Kiwi Browser:** Full Chrome extension + UserScript support +- **Microsoft Edge Mobile:** Tampermonkey support +- **Firefox Mobile:** Greasemonkey, Tampermonkey, Violentmonkey +- **Yandex Browser:** Chrome extension support + +**iOS:** + +- **Safari Mobile:** Tampermonkey or Userscripts App +- Limited support due to iOS restrictions + +### Mobile Features + +• **Touch Gestures:** Tap, swipe, and pinch detection +• **Responsive Design:** Mobile-first CSS with viewport adaptation +• **Safe Area Support:** Automatic handling of notched devices +• **Orientation Detection:** Portrait/landscape change handling +• **Mobile-Optimized UI:** Touch-friendly buttons and menus + ## Contributing 1. Fork the repository diff --git a/header.config.json b/header.config.json index c7dca08..82b3eff 100644 --- a/header.config.json +++ b/header.config.json @@ -1,50 +1,58 @@ { - "updateUrl": "", - "downloadUrl": "", - "supportUrl": "", - "iconUrl": "./assets/icon.png", - "environments": { - "development": { - "includes": [ - "http://localhost:*/*", - "https://localhost:*/*", - "https://*.josunlp.de/*", - "https://josunlp.de/*" - ], - "excludes": [], - "grants": [ - "GM_setValue", - "GM_getValue", - "GM_deleteValue", - "GM_listValues", - "GM_log", - "GM_notification", - "GM_registerMenuCommand", - "GM_unregisterMenuCommand" - ] - }, - "production": { - "includes": [ - "https://*.josunlp.de/*", - "https://josunlp.de/*" - ], - "excludes": [], - "grants": [ - "GM_setValue", - "GM_getValue", - "GM_deleteValue", - "GM_listValues", - "GM_registerMenuCommand" - ] - } + "updateUrl": "", + "downloadUrl": "", + "supportUrl": "", + "iconUrl": "./assets/icon.png", + "environments": { + "development": { + "includes": [ + "http://localhost:*/*", + "https://localhost:*/*", + "https://*.josunlp.de/*", + "https://josunlp.de/*" + ], + "excludes": [], + "grants": [ + "GM_setValue", + "GM_getValue", + "GM_deleteValue", + "GM_listValues", + "GM_log", + "GM_notification", + "GM_registerMenuCommand", + "GM_unregisterMenuCommand" + ] }, - "requires": [], - "resources": [], - "connecters": [], - "matches": [], - "matchAllFrames": false, - "runAt": "document-start", - "antifeatures": [], - "noframes": false, - "unwrap": false + "production": { + "includes": ["https://*.josunlp.de/*", "https://josunlp.de/*"], + "excludes": [], + "grants": [ + "GM_setValue", + "GM_getValue", + "GM_deleteValue", + "GM_listValues", + "GM_registerMenuCommand" + ] + } + }, + "mobile": { + "supportedBrowsers": [ + "Kiwi Browser (Android)", + "Microsoft Edge Mobile", + "Firefox Mobile", + "Safari Mobile (iOS)", + "Yandex Browser" + ], + "touchOptimized": true, + "responsiveDesign": true + }, + "requires": [], + "resources": [], + "connecters": [], + "matches": [], + "matchAllFrames": false, + "runAt": "document-start", + "antifeatures": [], + "noframes": false, + "unwrap": false } diff --git a/package.json b/package.json index 0277e41..2cc8412 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "userscript-project-template", "version": "1.0.0", - "description": "A modern, production-ready template for building UserScripts using TypeScript and Vite. This template provides a solid foundation with best practices, type safety, and modern development tools for Tampermonkey and Greasemonkey scripts.", + "description": "A modern, production-ready template for building UserScripts using TypeScript and Vite with mobile browser support, touch gestures, and responsive design for Tampermonkey and Greasemonkey scripts.", "main": "index.ts", "module": "node", "scripts": { @@ -32,7 +32,13 @@ "vite", "browser-automation", "web-enhancement", - "browser-scripting" + "browser-scripting", + "mobile-support", + "touch-gestures", + "responsive-design", + "mobile-browser", + "kiwi-browser", + "edge-mobile" ], "author": "Jonas Pfalzgraf ", "license": "MIT", diff --git a/src/index.ts b/src/index.ts index 2c54228..933ce8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { ExampleModule } from '@/modules/example'; import { DOMUtils } from '@/utils/dom'; import { EventEmitter } from '@/utils/events'; +import { MobileUtils } from '@/utils/mobile'; import { Storage } from '@/utils/storage'; /** @@ -60,6 +61,14 @@ class App extends EventEmitter { protected async main(): Promise { console.log('👋 Hello from UserScript Template!'); + // Mobile detection and setup + const mobileInfo = MobileUtils.detect(); + if (mobileInfo.isMobile) { + console.log('📱 Mobile browser detected:', mobileInfo.browser); + MobileUtils.addMobileStyles(); + MobileUtils.logMobileInfo(); + } + // Example: Add some basic functionality this.addExampleFeatures(); @@ -75,11 +84,27 @@ class App extends EventEmitter { // Initialize example module const exampleModule = new ExampleModule(); await exampleModule.initialize(); - - // Register the module this.registerModule('example', exampleModule); - // Listen to module events + // Initialize mobile module if on mobile device + const mobileInfo = MobileUtils.detect(); + if (mobileInfo.isMobile || mobileInfo.hasTouch) { + const { MobileModule } = await import('@/modules/mobile'); + const mobileModule = new MobileModule(); + await mobileModule.initialize(); + this.registerModule('mobile', mobileModule); + + // Listen to mobile module events + mobileModule.on('gestureDetected', ({ type, position }) => { + console.log(`📱 Gesture detected: ${type} at ${position.x}, ${position.y}`); + }); + + mobileModule.on('orientationChanged', ({ orientation }) => { + console.log(`📱 Orientation changed to: ${orientation}`); + }); + } + + // Listen to example module events exampleModule.on('actionPerformed', ({ action, timestamp }) => { console.log( `📡 Module action received: ${action} at ${new Date(timestamp).toLocaleString()}` @@ -94,28 +119,72 @@ class App extends EventEmitter { * Example features to demonstrate the template */ private addExampleFeatures(): void { - // Example: Add custom styles - DOMUtils.addStyles( - ` + // Add mobile-optimized styles + const mobileInfo = MobileUtils.detect(); + const baseCss = ` .userscript-highlight { background-color: yellow !important; border: 2px solid red !important; } - `, - 'userscript-styles' - ); + `; + + // Add mobile-specific styles if on mobile + const mobileCss = mobileInfo.isMobile + ? ` + .userscript-highlight { + padding: 8px !important; + border-radius: 4px !important; + font-size: 16px !important; /* Prevent zoom on iOS */ + } + ` + : ''; + + DOMUtils.addStyles(baseCss + mobileCss, 'userscript-styles'); // Example: Storage usage const visitCount = (Storage.get('visitCount', 0) || 0) + 1; Storage.set('visitCount', visitCount); console.log(`📊 This is visit #${visitCount}`); - // Example: Add menu command + // Example: Add menu command (with mobile detection) if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('Show Visit Count', () => { const count = Storage.get('visitCount', 0); - alert(`You have visited this page ${count} times!`); + + if (mobileInfo.isMobile) { + // Mobile-friendly notification + if (typeof GM_notification !== 'undefined') { + GM_notification(`Visit Count: ${count}`, 'UserScript Info', undefined, () => { + console.log('Notification clicked'); + }); + } else { + alert(`You have visited this page ${count} times!`); + } + } else { + // Desktop alert + alert(`You have visited this page ${count} times!`); + } }); + + // Add mobile-specific menu command + if (mobileInfo.isMobile) { + GM_registerMenuCommand('Mobile Info', () => { + MobileUtils.logMobileInfo(); + + const info = ` +Device: ${mobileInfo.isAndroid ? 'Android' : mobileInfo.isIOS ? 'iOS' : 'Other'} +Touch Support: ${mobileInfo.hasTouch ? 'Yes' : 'No'} +UserScript Support: ${MobileUtils.supportsUserScripts() ? 'Yes' : 'No'} +Recommended Manager: ${MobileUtils.getRecommendedUserScriptManager()} + `; + + if (typeof GM_notification !== 'undefined') { + GM_notification(info, 'Mobile Device Info'); + } else { + alert(info); + } + }); + } } } diff --git a/src/modules/mobile.ts b/src/modules/mobile.ts new file mode 100644 index 0000000..6572447 --- /dev/null +++ b/src/modules/mobile.ts @@ -0,0 +1,276 @@ +import { EventEmitter } from '@/utils/events'; +import { MobileUtils } from '@/utils/mobile'; + +/** + * Mobile-specific module events + */ +interface MobileModuleEvents { + gestureDetected: { type: 'tap' | 'swipe' | 'pinch'; position: { x: number; y: number } }; + orientationChanged: { orientation: 'portrait' | 'landscape' }; + touchStart: { touches: number }; + touchEnd: { touches: number }; +} + +/** + * Mobile interaction module for touch gestures and mobile-specific features + */ +export class MobileModule extends EventEmitter { + private isInitialized = false; + private swipeThreshold = 50; + private tapTimeout = 300; + private lastTap = 0; + private touchStartPos: { x: number; y: number } | null = null; + + /** + * Initialize the mobile module + */ + async initialize(): Promise { + if (this.isInitialized) { + return; + } + + const detection = MobileUtils.detect(); + + if (!detection.hasTouch) { + console.log('⚠️ Mobile module: No touch support detected'); + return; + } + + console.log('📱 Initializing mobile module...'); + + this.setupTouchEvents(); + this.setupOrientationDetection(); + + this.isInitialized = true; + console.log('✅ Mobile module initialized'); + } + + /** + * Setup touch event handlers + */ + private setupTouchEvents(): void { + // Handle touch start + document.addEventListener( + 'touchstart', + event => { + const position = MobileUtils.getEventPosition(event); + this.touchStartPos = { x: position.x, y: position.y }; + + this.emit('touchStart', { touches: event.touches.length }); + + // Detect taps + this.detectTap(event); + }, + { passive: true } + ); + + // Handle touch move for swipe detection + document.addEventListener( + 'touchmove', + event => { + if (this.touchStartPos && event.touches.length === 1) { + this.detectSwipe(event); + } + }, + { passive: true } + ); + + // Handle touch end + document.addEventListener( + 'touchend', + event => { + this.emit('touchEnd', { touches: event.touches.length }); + this.touchStartPos = null; + }, + { passive: true } + ); + + // Handle pinch gestures + document.addEventListener('gesturestart', event => { + event.preventDefault(); + const position = MobileUtils.getEventPosition(event as any); + this.emit('gestureDetected', { + type: 'pinch', + position: { x: position.x, y: position.y }, + }); + }); + } + + /** + * Setup orientation change detection + */ + private setupOrientationDetection(): void { + const handleOrientationChange = () => { + const orientation = MobileUtils.isPortrait() ? 'portrait' : 'landscape'; + this.emit('orientationChanged', { orientation }); + console.log(`📱 Orientation changed to: ${orientation}`); + }; + + // Listen for orientation changes + window.addEventListener('orientationchange', handleOrientationChange); + window.addEventListener('resize', handleOrientationChange); + + // Initial orientation + setTimeout(handleOrientationChange, 100); + } + + /** + * Detect tap gestures + */ + private detectTap(event: TouchEvent): void { + const currentTime = Date.now(); + const position = MobileUtils.getEventPosition(event); + + if (currentTime - this.lastTap < this.tapTimeout) { + // Double tap detected + this.emit('gestureDetected', { + type: 'tap', + position: { x: position.x, y: position.y }, + }); + } + + this.lastTap = currentTime; + + // Single tap with delay + setTimeout(() => { + if (Date.now() - this.lastTap >= this.tapTimeout) { + this.emit('gestureDetected', { + type: 'tap', + position: { x: position.x, y: position.y }, + }); + } + }, this.tapTimeout); + } + + /** + * Detect swipe gestures + */ + private detectSwipe(event: TouchEvent): void { + if (!this.touchStartPos) return; + + const position = MobileUtils.getEventPosition(event); + const deltaX = position.x - this.touchStartPos.x; + const deltaY = position.y - this.touchStartPos.y; + + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + if (distance > this.swipeThreshold) { + this.emit('gestureDetected', { + type: 'swipe', + position: { x: position.x, y: position.y }, + }); + this.touchStartPos = null; // Reset to prevent multiple swipe events + } + } + + /** + * Create mobile-friendly button + */ + createMobileButton(text: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement('button'); + button.textContent = text; + button.className = 'userscript-mobile-button'; + + // Add unified event listener for touch and mouse + MobileUtils.addUnifiedEventListener(button, 'start', event => { + event.preventDefault(); + onClick(); + }); + + // Prevent double-tap zoom + MobileUtils.preventDoubleTapZoom(button); + + return button; + } + + /** + * Create mobile-friendly menu + */ + createMobileMenu(items: Array<{ text: string; action: () => void }>): HTMLElement { + const menu = document.createElement('div'); + menu.className = 'userscript-mobile-menu'; + + // Add safe area padding for devices with notches + const safeArea = MobileUtils.getSafeAreaInsets(); + menu.style.paddingTop = `${Math.max(16, safeArea.top)}px`; + menu.style.paddingBottom = `${Math.max(16, safeArea.bottom)}px`; + menu.style.paddingLeft = `${Math.max(16, safeArea.left)}px`; + menu.style.paddingRight = `${Math.max(16, safeArea.right)}px`; + + items.forEach(item => { + const button = this.createMobileButton(item.text, () => { + item.action(); + this.hideMobileMenu(menu); + }); + menu.appendChild(button); + }); + + return menu; + } + + /** + * Show mobile menu at position + */ + showMobileMenu(menu: HTMLElement, position?: { x: number; y: number }): void { + document.body.appendChild(menu); + + if (position) { + const detection = MobileUtils.detect(); + + if (detection.isMobile) { + // On mobile, show at bottom of screen + menu.style.position = 'fixed'; + menu.style.bottom = '16px'; + menu.style.left = '16px'; + menu.style.right = '16px'; + } else { + // On desktop, show at cursor position + menu.style.position = 'fixed'; + menu.style.left = `${position.x}px`; + menu.style.top = `${position.y}px`; + } + } + + // Add backdrop + const backdrop = document.createElement('div'); + backdrop.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.3); + z-index: 9999; + `; + + backdrop.addEventListener('click', () => { + this.hideMobileMenu(menu); + }); + + document.body.appendChild(backdrop); + menu.dataset.backdrop = 'true'; + } + + /** + * Hide mobile menu + */ + hideMobileMenu(menu: HTMLElement): void { + if (menu.dataset.backdrop) { + const backdrop = document.querySelector('div[style*="rgba(0, 0, 0, 0.3)"]'); + if (backdrop) { + backdrop.remove(); + } + } + + if (menu.parentNode) { + menu.parentNode.removeChild(menu); + } + } + + /** + * Get module status + */ + get initialized(): boolean { + return this.isInitialized; + } +} diff --git a/src/utils/mobile.ts b/src/utils/mobile.ts new file mode 100644 index 0000000..234890d --- /dev/null +++ b/src/utils/mobile.ts @@ -0,0 +1,365 @@ +/** + * Mobile browser detection and touch event utilities for UserScript development + */ + +/** + * Mobile browser detection interface + */ +interface MobileDetection { + isMobile: boolean; + isAndroid: boolean; + isIOS: boolean; + isTablet: boolean; + hasTouch: boolean; + userAgent: string; + browser: { + isKiwi: boolean; + isEdgeMobile: boolean; + isFirefoxMobile: boolean; + isSafariMobile: boolean; + isYandex: boolean; + }; +} + +/** + * Touch event handler interface + */ +interface TouchPosition { + x: number; + y: number; + identifier: number; +} + +/** + * Mobile utility class for UserScript development + */ +export class MobileUtils { + private static _detection: MobileDetection | null = null; + + /** + * Detect mobile browser and capabilities + */ + static detect(): MobileDetection { + if (this._detection) { + return this._detection; + } + + const ua = navigator.userAgent.toLowerCase(); + const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + + // Mobile OS detection + const isAndroid = /android/.test(ua); + const isIOS = /ipad|iphone|ipod/.test(ua); + const isTablet = /ipad/.test(ua) || (isAndroid && !/mobile/.test(ua)); + + // Mobile browser detection + const isKiwi = /kiwi/.test(ua) || (/chrome/.test(ua) && isAndroid); + const isEdgeMobile = /edg\//.test(ua) && (isAndroid || isIOS); + const isFirefoxMobile = /firefox/.test(ua) && (isAndroid || isIOS); + const isSafariMobile = /safari/.test(ua) && isIOS && !/chrome|crios|fxios/.test(ua); + const isYandex = /yabrowser/.test(ua); + + const isMobile = + isAndroid || + isIOS || + hasTouch || + /mobile|phone|android|iphone|ipod|blackberry|windows phone/.test(ua); + + this._detection = { + isMobile, + isAndroid, + isIOS, + isTablet, + hasTouch, + userAgent: navigator.userAgent, + browser: { + isKiwi, + isEdgeMobile, + isFirefoxMobile, + isSafariMobile, + isYandex, + }, + }; + + return this._detection; + } + + /** + * Check if current browser supports UserScripts + */ + static supportsUserScripts(): boolean { + const detection = this.detect(); + + // Desktop browsers - full support + if (!detection.isMobile) { + return true; + } + + // Mobile browsers with known UserScript support + return ( + detection.browser.isKiwi || + detection.browser.isEdgeMobile || + detection.browser.isFirefoxMobile || + detection.browser.isSafariMobile || + detection.browser.isYandex + ); + } + + /** + * Get recommended UserScript manager for current browser + */ + static getRecommendedUserScriptManager(): string { + const detection = this.detect(); + + if (!detection.isMobile) { + return 'Tampermonkey (Desktop)'; + } + + if (detection.browser.isKiwi) { + return 'Built-in Chrome Extension support'; + } + if (detection.browser.isEdgeMobile) { + return 'Tampermonkey for Edge Mobile'; + } + if (detection.browser.isFirefoxMobile) { + return 'Greasemonkey or Tampermonkey'; + } + if (detection.browser.isSafariMobile) { + return 'Tampermonkey or Userscripts App'; + } + if (detection.browser.isYandex) { + return 'Built-in extension support'; + } + + return 'Not supported'; + } + + /** + * Normalize touch/mouse event to get position + */ + static getEventPosition(event: TouchEvent | MouseEvent): TouchPosition { + if ('touches' in event && event.touches.length > 0) { + const touch = event.touches[0]; + return { + x: touch.clientX, + y: touch.clientY, + identifier: touch.identifier, + }; + } else if ('clientX' in event) { + return { + x: event.clientX, + y: event.clientY, + identifier: 0, + }; + } + return { x: 0, y: 0, identifier: 0 }; + } + + /** + * Get all touch positions from touch event + */ + static getAllTouchPositions(event: TouchEvent): TouchPosition[] { + const positions: TouchPosition[] = []; + for (let i = 0; i < event.touches.length; i++) { + const touch = event.touches[i]; + positions.push({ + x: touch.clientX, + y: touch.clientY, + identifier: touch.identifier, + }); + } + return positions; + } + + /** + * Add unified touch/mouse event listener + */ + static addUnifiedEventListener( + element: Element, + eventType: 'start' | 'move' | 'end', + handler: (event: TouchEvent | MouseEvent) => void, + options?: AddEventListenerOptions + ): () => void { + const detection = this.detect(); + const removeListeners: (() => void)[] = []; + + if (detection.hasTouch) { + const touchEvent = { + start: 'touchstart', + move: 'touchmove', + end: 'touchend', + }[eventType]; + + element.addEventListener(touchEvent, handler as EventListener, options); + removeListeners.push(() => { + element.removeEventListener(touchEvent, handler as EventListener, options); + }); + } + + // Always add mouse events as fallback + const mouseEvent = { + start: 'mousedown', + move: 'mousemove', + end: 'mouseup', + }[eventType]; + + element.addEventListener(mouseEvent, handler as EventListener, options); + removeListeners.push(() => { + element.removeEventListener(mouseEvent, handler as EventListener, options); + }); + + return () => { + removeListeners.forEach(remove => remove()); + }; + } + + /** + * Add CSS for mobile-friendly interface + */ + static addMobileStyles(): void { + const detection = this.detect(); + + if (!detection.isMobile) { + return; + } + + const css = ` + /* Mobile-first UserScript styles */ + .userscript-mobile-container { + touch-action: manipulation; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + } + + .userscript-mobile-button { + min-height: 44px; /* iOS minimum touch target */ + min-width: 44px; + padding: 12px 16px; + font-size: 16px; /* Prevents zoom on iOS */ + border-radius: 8px; + cursor: pointer; + touch-action: manipulation; + } + + .userscript-mobile-input { + font-size: 16px; /* Prevents zoom on iOS */ + padding: 12px; + border-radius: 4px; + } + + .userscript-mobile-menu { + position: fixed; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + z-index: 10000; + max-width: 90vw; + max-height: 80vh; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + /* Dark mode support */ + @media (prefers-color-scheme: dark) { + .userscript-mobile-menu { + background: rgba(40, 40, 40, 0.95); + color: white; + } + } + + /* Tablet optimizations */ + @media (min-width: 768px) and (max-width: 1024px) { + .userscript-mobile-container { + max-width: 600px; + margin: 0 auto; + } + } + + /* Phone optimizations */ + @media (max-width: 767px) { + .userscript-mobile-container { + padding: 16px; + } + + .userscript-mobile-menu { + left: 8px !important; + right: 8px !important; + bottom: 8px !important; + top: auto !important; + max-height: 60vh; + } + } + `; + + const styleElement = document.createElement('style'); + styleElement.id = 'userscript-mobile-styles'; + styleElement.textContent = css; + (document.head || document.documentElement).appendChild(styleElement); + } + + /** + * Check if viewport is in portrait mode + */ + static isPortrait(): boolean { + return window.innerHeight > window.innerWidth; + } + + /** + * Check if viewport is in landscape mode + */ + static isLandscape(): boolean { + return window.innerWidth > window.innerHeight; + } + + /** + * Get safe area insets (for devices with notches) + */ + static getSafeAreaInsets(): { top: number; bottom: number; left: number; right: number } { + const computedStyle = getComputedStyle(document.documentElement); + + return { + top: parseInt(computedStyle.getPropertyValue('env(safe-area-inset-top)') || '0'), + bottom: parseInt(computedStyle.getPropertyValue('env(safe-area-inset-bottom)') || '0'), + left: parseInt(computedStyle.getPropertyValue('env(safe-area-inset-left)') || '0'), + right: parseInt(computedStyle.getPropertyValue('env(safe-area-inset-right)') || '0'), + }; + } + + /** + * Prevent page zoom when double-tapping elements + */ + static preventDoubleTapZoom(element: Element): void { + let lastTap = 0; + + element.addEventListener('touchend', event => { + const currentTime = new Date().getTime(); + const tapLength = currentTime - lastTap; + + if (tapLength < 500 && tapLength > 0) { + event.preventDefault(); + } + + lastTap = currentTime; + }); + } + + /** + * Log mobile detection information for debugging + */ + static logMobileInfo(): void { + const detection = this.detect(); + const manager = this.getRecommendedUserScriptManager(); + + console.group('📱 Mobile Detection Info'); + console.log('Is Mobile:', detection.isMobile); + console.log('Platform:', detection.isAndroid ? 'Android' : detection.isIOS ? 'iOS' : 'Desktop'); + console.log('Has Touch:', detection.hasTouch); + console.log('Browser:', detection.browser); + console.log('UserScript Support:', this.supportsUserScripts()); + console.log('Recommended Manager:', manager); + console.log('User Agent:', detection.userAgent); + console.groupEnd(); + } +}