mirror of
https://github.com/JosunLP/UserScriptProjectTemplate.git
synced 2025-10-14 09:00:11 +00:00
365 lines
9.4 KiB
TypeScript
365 lines
9.4 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|
|
}
|