feat: Implement mobile support with touch gestures and responsive design features

This commit is contained in:
JonasPfalzgraf 2025-07-11 19:21:39 +02:00
parent 884aed648f
commit ec933c3545
6 changed files with 842 additions and 60 deletions

View file

@ -20,6 +20,9 @@ A modern, production-ready template for building UserScripts using TypeScript an
• 🎨 **DOM Utilities:** Helper functions for element manipulation and waiting • 🎨 **DOM Utilities:** Helper functions for element manipulation and waiting
• 🔒 **Error Handling:** Comprehensive error boundary system • 🔒 **Error Handling:** Comprehensive error boundary system
• ⚡ **Event System:** Type-safe event emitter for module communication • ⚡ **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 ## Installation
@ -202,6 +205,39 @@ export class MyModule extends EventEmitter<ModuleEvents> {
} }
``` ```
### 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 ## UserScript Compatibility
**Tampermonkey:** Full support with all GM\_\* APIs **Tampermonkey:** Full support with all GM\_\* APIs
@ -209,6 +245,28 @@ export class MyModule extends EventEmitter<ModuleEvents> {
**Violentmonkey:** Full compatibility **Violentmonkey:** Full compatibility
**Safari:** Works with userscript managers **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 ## Contributing
1. Fork the repository 1. Fork the repository

View file

@ -24,10 +24,7 @@
] ]
}, },
"production": { "production": {
"includes": [ "includes": ["https://*.josunlp.de/*", "https://josunlp.de/*"],
"https://*.josunlp.de/*",
"https://josunlp.de/*"
],
"excludes": [], "excludes": [],
"grants": [ "grants": [
"GM_setValue", "GM_setValue",
@ -38,6 +35,17 @@
] ]
} }
}, },
"mobile": {
"supportedBrowsers": [
"Kiwi Browser (Android)",
"Microsoft Edge Mobile",
"Firefox Mobile",
"Safari Mobile (iOS)",
"Yandex Browser"
],
"touchOptimized": true,
"responsiveDesign": true
},
"requires": [], "requires": [],
"resources": [], "resources": [],
"connecters": [], "connecters": [],

View file

@ -1,7 +1,7 @@
{ {
"name": "userscript-project-template", "name": "userscript-project-template",
"version": "1.0.0", "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", "main": "index.ts",
"module": "node", "module": "node",
"scripts": { "scripts": {
@ -32,7 +32,13 @@
"vite", "vite",
"browser-automation", "browser-automation",
"web-enhancement", "web-enhancement",
"browser-scripting" "browser-scripting",
"mobile-support",
"touch-gestures",
"responsive-design",
"mobile-browser",
"kiwi-browser",
"edge-mobile"
], ],
"author": "Jonas Pfalzgraf <info@josunlp.de>", "author": "Jonas Pfalzgraf <info@josunlp.de>",
"license": "MIT", "license": "MIT",

View file

@ -1,6 +1,7 @@
import { ExampleModule } from '@/modules/example'; import { ExampleModule } from '@/modules/example';
import { DOMUtils } from '@/utils/dom'; import { DOMUtils } from '@/utils/dom';
import { EventEmitter } from '@/utils/events'; import { EventEmitter } from '@/utils/events';
import { MobileUtils } from '@/utils/mobile';
import { Storage } from '@/utils/storage'; import { Storage } from '@/utils/storage';
/** /**
@ -60,6 +61,14 @@ class App extends EventEmitter<AppEvents> {
protected async main(): Promise<void> { protected async main(): Promise<void> {
console.log('👋 Hello from UserScript Template!'); 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 // Example: Add some basic functionality
this.addExampleFeatures(); this.addExampleFeatures();
@ -75,11 +84,27 @@ class App extends EventEmitter<AppEvents> {
// Initialize example module // Initialize example module
const exampleModule = new ExampleModule(); const exampleModule = new ExampleModule();
await exampleModule.initialize(); await exampleModule.initialize();
// Register the module
this.registerModule('example', exampleModule); 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 }) => { exampleModule.on('actionPerformed', ({ action, timestamp }) => {
console.log( console.log(
`📡 Module action received: ${action} at ${new Date(timestamp).toLocaleString()}` `📡 Module action received: ${action} at ${new Date(timestamp).toLocaleString()}`
@ -94,28 +119,72 @@ class App extends EventEmitter<AppEvents> {
* Example features to demonstrate the template * Example features to demonstrate the template
*/ */
private addExampleFeatures(): void { private addExampleFeatures(): void {
// Example: Add custom styles // Add mobile-optimized styles
DOMUtils.addStyles( const mobileInfo = MobileUtils.detect();
` const baseCss = `
.userscript-highlight { .userscript-highlight {
background-color: yellow !important; background-color: yellow !important;
border: 2px solid red !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 // Example: Storage usage
const visitCount = (Storage.get<number>('visitCount', 0) || 0) + 1; const visitCount = (Storage.get<number>('visitCount', 0) || 0) + 1;
Storage.set('visitCount', visitCount); Storage.set('visitCount', visitCount);
console.log(`📊 This is visit #${visitCount}`); console.log(`📊 This is visit #${visitCount}`);
// Example: Add menu command // Example: Add menu command (with mobile detection)
if (typeof GM_registerMenuCommand !== 'undefined') { if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand('Show Visit Count', () => { GM_registerMenuCommand('Show Visit Count', () => {
const count = Storage.get<number>('visitCount', 0); const count = Storage.get<number>('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);
}
});
}
} }
} }

276
src/modules/mobile.ts Normal file
View file

@ -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<MobileModuleEvents> {
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<void> {
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;
}
}

365
src/utils/mobile.ts Normal file
View file

@ -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();
}
}