mirror of
https://github.com/JosunLP/UserScriptProjectTemplate.git
synced 2025-12-06 14:40:05 +00:00
Compare commits
No commits in common. "dcb2a449aa105df12784f2ceee9a617545dd361f" and "8089771d41963b21218abc65f5e989f53c47042a" have entirely different histories.
dcb2a449aa
...
8089771d41
20 changed files with 620 additions and 4977 deletions
|
|
@ -1,24 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.json]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.ts]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.js]
|
|
||||||
indent_size = 2
|
|
||||||
39
.eslintrc.js
39
.eslintrc.js
|
|
@ -1,39 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
extends: ['eslint:recommended'],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es6: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
// Greasemonkey/Tampermonkey globals
|
|
||||||
GM_setValue: 'readonly',
|
|
||||||
GM_getValue: 'readonly',
|
|
||||||
GM_deleteValue: 'readonly',
|
|
||||||
GM_listValues: 'readonly',
|
|
||||||
GM_log: 'readonly',
|
|
||||||
GM_notification: 'readonly',
|
|
||||||
GM_openInTab: 'readonly',
|
|
||||||
GM_registerMenuCommand: 'readonly',
|
|
||||||
GM_unregisterMenuCommand: 'readonly',
|
|
||||||
GM_xmlhttpRequest: 'readonly',
|
|
||||||
GM_info: 'readonly',
|
|
||||||
unsafeWindow: 'readonly',
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-console': 'off',
|
|
||||||
'no-debugger': 'warn',
|
|
||||||
'prefer-const': 'error',
|
|
||||||
'no-var': 'error',
|
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'no-undef': 'off',
|
|
||||||
},
|
|
||||||
ignorePatterns: ['dist/', 'node_modules/', 'tools/**/*.js'],
|
|
||||||
};
|
|
||||||
20
.gitignore
vendored
20
.gitignore
vendored
|
|
@ -643,23 +643,5 @@ FodyWeavers.xsd
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,webstorm,intellij,sublimetext
|
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,webstorm,intellij,sublimetext
|
||||||
|
|
||||||
# Project specific
|
dist
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
tools/userScriptHeader.js
|
tools/userScriptHeader.js
|
||||||
|
|
||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# TypeScript
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 100,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "avoid",
|
|
||||||
"endOfLine": "lf"
|
|
||||||
}
|
|
||||||
317
README.md
317
README.md
|
|
@ -1,316 +1,3 @@
|
||||||
# UserScript Project Template
|
# UserScriptProjectTemplate
|
||||||
|
|
||||||
[](https://github.com/JosunLP/UserScriptProjectTemplate/blob/main/LICENSE)
|
A user script project template to create large and structured TypeScript projects for Tampermonkey or Greasemonkey. It is intended to form a scalable base and is primarily aimed at the Ingress community.
|
||||||
[](https://github.com/JosunLP/UserScriptProjectTemplate/issues)
|
|
||||||
[](https://github.com/JosunLP/UserScriptProjectTemplate/stargazers)
|
|
||||||
|
|
||||||
## 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 creating sophisticated Tampermonkey and Greasemonkey scripts.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
• 🚀 **Modern Tech Stack:** TypeScript, Vite, ESLint, Prettier
|
|
||||||
• 🛡️ **Type Safety:** Strict TypeScript configuration with comprehensive UserScript API definitions
|
|
||||||
• 🔧 **Development Tools:** ESLint, Prettier, automated build pipeline
|
|
||||||
• 🎯 **Environment Support:** Separate development and production configurations
|
|
||||||
• 📦 **Modular Architecture:** Component system with reusable utilities
|
|
||||||
• 💾 **Storage Management:** Type-safe wrapper for GM_setValue/GM_getValue
|
|
||||||
• 🛠️ **Build System:** Optimized Vite configuration with automatic header generation
|
|
||||||
• 🎨 **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
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/JosunLP/UserScriptProjectTemplate.git
|
|
||||||
cd UserScriptProjectTemplate
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Start development mode with auto-rebuild
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
npm run type-check
|
|
||||||
|
|
||||||
# Linting and formatting
|
|
||||||
npm run validate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```bash
|
|
||||||
src/
|
|
||||||
├── types/ # TypeScript type definitions
|
|
||||||
├── utils/ # Utility functions (Storage, DOM, Events)
|
|
||||||
├── core/ # Core application logic
|
|
||||||
├── modules/ # Feature modules
|
|
||||||
└── index.ts # Main application entry point
|
|
||||||
|
|
||||||
tools/
|
|
||||||
├── userScriptHeader.ts # UserScript header generator
|
|
||||||
└── userScriptHeader.js # Compiled header generator
|
|
||||||
|
|
||||||
assets/ # Icons and static resources
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
The main configuration is in `header.config.json`. This file controls UserScript metadata generation:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"environments": {
|
|
||||||
"development": {
|
|
||||||
"includes": ["http://localhost:*/*", "https://localhost:*/*"],
|
|
||||||
"grants": ["GM_setValue", "GM_getValue", "GM_log", "GM_notification"]
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"includes": ["https://your-domain.com/*"],
|
|
||||||
"grants": ["GM_setValue", "GM_getValue"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
npm run dev # Start development with watch mode
|
|
||||||
npm run dev:build # Single development build with header
|
|
||||||
npm run dev:header # Generate header for existing dev build
|
|
||||||
|
|
||||||
# Production
|
|
||||||
npm run build # Build for production
|
|
||||||
npm run build:prod # Explicit production build
|
|
||||||
|
|
||||||
# Quality Assurance
|
|
||||||
npm run validate # Type check + lint
|
|
||||||
npm run lint # ESLint with auto-fix
|
|
||||||
npm run format # Prettier formatting
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
npm run clean # Clean dist folder
|
|
||||||
npm run type-check # TypeScript type checking
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
|
|
||||||
1. **Configure your script** in `header.config.json`
|
|
||||||
2. **Start development:** `npm run dev`
|
|
||||||
3. **Write your code** in the `src/` directory
|
|
||||||
4. **Build for production:** `npm run build`
|
|
||||||
5. **Install the UserScript** from `dist/` folder in Tampermonkey/Greasemonkey
|
|
||||||
|
|
||||||
### Storage Management
|
|
||||||
|
|
||||||
The template includes a type-safe storage system:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Storage } from '@/utils/storage';
|
|
||||||
|
|
||||||
// Save data
|
|
||||||
Storage.set('userData', { name: 'John', visits: 5 });
|
|
||||||
|
|
||||||
// Get data with default value
|
|
||||||
const userData = Storage.get('userData', { name: '', visits: 0 });
|
|
||||||
|
|
||||||
// Check if key exists
|
|
||||||
if (Storage.has('userData')) {
|
|
||||||
// Key exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove data
|
|
||||||
Storage.remove('userData');
|
|
||||||
```
|
|
||||||
|
|
||||||
### DOM Utilities
|
|
||||||
|
|
||||||
Helper functions for DOM manipulation:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { DOMUtils } from '@/utils/dom';
|
|
||||||
|
|
||||||
// Wait for element to appear
|
|
||||||
const element = await DOMUtils.waitForElement('.my-selector');
|
|
||||||
|
|
||||||
// Add custom styles
|
|
||||||
DOMUtils.addStyles(`
|
|
||||||
.my-class { color: red; }
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create element with attributes
|
|
||||||
const button = DOMUtils.createElement('button', {
|
|
||||||
textContent: 'Click me',
|
|
||||||
onclick: () => console.log('Clicked!'),
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Event System
|
|
||||||
|
|
||||||
Type-safe communication between modules:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { EventEmitter } from '@/utils/events';
|
|
||||||
|
|
||||||
interface MyEvents {
|
|
||||||
userAction: { userId: string };
|
|
||||||
dataLoaded: { count: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
const emitter = new EventEmitter<MyEvents>();
|
|
||||||
|
|
||||||
// Listen for events
|
|
||||||
emitter.on('userAction', ({ userId }) => {
|
|
||||||
console.log(`User ${userId} performed an action`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit events
|
|
||||||
emitter.emit('userAction', { userId: '123' });
|
|
||||||
```
|
|
||||||
|
|
||||||
### Module System
|
|
||||||
|
|
||||||
Create reusable, event-driven modules:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { EventEmitter } from '@/utils/events';
|
|
||||||
|
|
||||||
interface ModuleEvents {
|
|
||||||
initialized: void;
|
|
||||||
actionPerformed: { action: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MyModule extends EventEmitter<ModuleEvents> {
|
|
||||||
async initialize() {
|
|
||||||
// Module initialization logic
|
|
||||||
this.emit('initialized', undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
• **Greasemonkey:** Compatible with standard UserScript APIs
|
|
||||||
• **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
|
|
||||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
|
||||||
3. Make your changes and ensure tests pass: `npm run validate`
|
|
||||||
4. Commit your changes: `git commit -m 'Add amazing feature'`
|
|
||||||
5. Push to the branch: `git push origin feature/amazing-feature`
|
|
||||||
6. Open a Pull Request
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
• Follow TypeScript best practices
|
|
||||||
• Use meaningful variable and function names
|
|
||||||
• Add proper error handling
|
|
||||||
• Write self-documenting code
|
|
||||||
• Follow the established project structure
|
|
||||||
• Run `npm run validate` before committing
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
**_Jonas Pfalzgraf_**
|
|
||||||
|
|
||||||
• Email: [info@josunlp.de](mailto:info@josunlp.de)
|
|
||||||
• GitHub: [@JosunLP](https://github.com/JosunLP)
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
### v0.0.1 (Current)
|
|
||||||
|
|
||||||
• ✨ Modern TypeScript setup with strict type checking
|
|
||||||
• 🛡️ Comprehensive UserScript API definitions
|
|
||||||
• 🎨 Modular architecture with utilities and components
|
|
||||||
• 🔧 ESLint and Prettier configuration
|
|
||||||
• 📦 Optimized Vite build system
|
|
||||||
• 🚀 Environment-based configuration
|
|
||||||
• 💾 Type-safe storage management
|
|
||||||
• 🎯 Event-driven module system
|
|
||||||
• ⚡ DOM manipulation utilities
|
|
||||||
• 🛠️ Automated header generation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**_Built with ❤️ for the UserScript community_**
|
|
||||||
|
|
|
||||||
BIN
assets/icon.png
BIN
assets/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
|
@ -1,58 +1,27 @@
|
||||||
{
|
{
|
||||||
"updateUrl": "",
|
"updateUrl": "",
|
||||||
"downloadUrl": "",
|
"downloadUrl": "",
|
||||||
"supportUrl": "",
|
"supportUrl": "",
|
||||||
"iconUrl": "./assets/icon.png",
|
"iconUrl": "./assets/icon.png",
|
||||||
"environments": {
|
"includes": [
|
||||||
"development": {
|
|
||||||
"includes": [
|
|
||||||
"http://localhost:*/*",
|
|
||||||
"https://localhost:*/*",
|
|
||||||
"https://*.josunlp.de/*",
|
"https://*.josunlp.de/*",
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mobile": {
|
|
||||||
"supportedBrowsers": [
|
|
||||||
"Kiwi Browser (Android)",
|
|
||||||
"Microsoft Edge Mobile",
|
|
||||||
"Firefox Mobile",
|
|
||||||
"Safari Mobile (iOS)",
|
|
||||||
"Yandex Browser"
|
|
||||||
],
|
],
|
||||||
"touchOptimized": true,
|
"excludes": [],
|
||||||
"responsiveDesign": true
|
"requires": [],
|
||||||
},
|
"resources": [],
|
||||||
"requires": [],
|
"connecters": [],
|
||||||
"resources": [],
|
"matches": [
|
||||||
"connecters": [],
|
"https://*.josunlp.de/*",
|
||||||
"matches": [],
|
"https://josunlp.de/*"
|
||||||
"matchAllFrames": false,
|
],
|
||||||
"runAt": "document-start",
|
"matchAllFrames": false,
|
||||||
"antifeatures": [],
|
"runAt": "document-start",
|
||||||
"noframes": false,
|
"grants": [
|
||||||
"unwrap": false
|
"GM_setValue",
|
||||||
|
"GM_getValue"
|
||||||
|
],
|
||||||
|
"antifeatures": [],
|
||||||
|
"noframes": false,
|
||||||
|
"unwrap": false
|
||||||
}
|
}
|
||||||
3435
package-lock.json
generated
3435
package-lock.json
generated
File diff suppressed because it is too large
Load diff
47
package.json
47
package.json
|
|
@ -1,44 +1,24 @@
|
||||||
{
|
{
|
||||||
"name": "userscript-project-template",
|
"name": "userscript-project-template",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
"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.",
|
"description": "A user script project template to create large and structured TypeScript projects for Tampermonkey or Greasemonkey. It is intended to form a scalable base and is primarily aimed at the Ingress community.",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"module": "node",
|
"module": "node",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --watch --mode development",
|
|
||||||
"dev:header": "npm run build-userScriptHeader",
|
|
||||||
"dev:build": "vite build --mode development && npm run build-userScriptHeader",
|
|
||||||
"build": "vite build && npm run build-userScriptHeader",
|
"build": "vite build && npm run build-userScriptHeader",
|
||||||
"build:prod": "vite build --mode production && npm run build-userScriptHeader",
|
|
||||||
"build-tooling": "tsc ./tools/userScriptHeader.ts --resolveJsonModule --esModuleInterop",
|
"build-tooling": "tsc ./tools/userScriptHeader.ts --resolveJsonModule --esModuleInterop",
|
||||||
"build-userScriptHeader": "npm run build-tooling && node ./tools/userScriptHeader.js",
|
"build-userScriptHeader": "npm run build-tooling && node ./tools/userScriptHeader.js"
|
||||||
"validate": "npm run type-check && npm run lint",
|
|
||||||
"lint": "eslint src/ --ext .ts,.tsx",
|
|
||||||
"lint:fix": "eslint src/ --ext .ts,.tsx --fix",
|
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,json}\"",
|
|
||||||
"type-check": "tsc --noEmit",
|
|
||||||
"clean": "rimraf dist"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/JosunLP/UserScriptProjectTemplate.git"
|
"url": "git+https://github.com/JosunLP/UserScriptProjectTemplate.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"userscript",
|
"User",
|
||||||
"tampermonkey",
|
"Script",
|
||||||
"greasemonkey",
|
"TypeScript",
|
||||||
"typescript",
|
"Webpack",
|
||||||
"template",
|
"Ingress"
|
||||||
"vite",
|
|
||||||
"browser-automation",
|
|
||||||
"web-enhancement",
|
|
||||||
"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",
|
||||||
|
|
@ -47,18 +27,11 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/JosunLP/UserScriptProjectTemplate#readme",
|
"homepage": "https://github.com/JosunLP/UserScriptProjectTemplate#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
|
||||||
"eslint": "^8.57.1",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-prettier": "^5.5.1",
|
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"rimraf": "^5.0.10",
|
|
||||||
"ts-loader": "^9.3.1",
|
"ts-loader": "^9.3.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.4.0",
|
"typescript": "^4.7.4",
|
||||||
"undici-types": "^6.18.2",
|
"undici-types": "^6.18.2",
|
||||||
"vite": "^7.0.4",
|
"vite": "^5.2.12",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"webpack": "^5.74.0",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^4.10.0"
|
"webpack-cli": "^4.10.0"
|
||||||
|
|
|
||||||
221
src/index.ts
221
src/index.ts
|
|
@ -1,216 +1,15 @@
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application events interface
|
* App
|
||||||
*/
|
*/
|
||||||
interface AppEvents {
|
class App {
|
||||||
ready: void;
|
|
||||||
error: Error;
|
constructor() {
|
||||||
moduleLoaded: { name: string };
|
this.main();
|
||||||
|
}
|
||||||
|
|
||||||
|
private main() {
|
||||||
|
console.log('Hello World!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Main Application Class
|
|
||||||
*
|
|
||||||
* This is the entry point for your UserScript.
|
|
||||||
* Extend this class to build your specific functionality.
|
|
||||||
*/
|
|
||||||
class App extends EventEmitter<AppEvents> {
|
|
||||||
private isInitialized = false;
|
|
||||||
private modules: Map<string, any> = new Map();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the application
|
|
||||||
*/
|
|
||||||
private async initialize(): Promise<void> {
|
|
||||||
try {
|
|
||||||
console.log('🚀 UserScript starting...');
|
|
||||||
|
|
||||||
// Wait for DOM to be ready
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
await new Promise(resolve => {
|
|
||||||
document.addEventListener('DOMContentLoaded', resolve, { once: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.main();
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
this.emit('ready', undefined);
|
|
||||||
|
|
||||||
console.log('✅ UserScript initialized successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ UserScript initialization failed:', error);
|
|
||||||
this.emit('error', error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main application logic
|
|
||||||
* Override this method in your implementation
|
|
||||||
*/
|
|
||||||
protected async main(): Promise<void> {
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Initialize modules
|
|
||||||
await this.initializeModules();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize application modules
|
|
||||||
*/
|
|
||||||
private async initializeModules(): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Initialize example module
|
|
||||||
const exampleModule = new ExampleModule();
|
|
||||||
await exampleModule.initialize();
|
|
||||||
this.registerModule('example', exampleModule);
|
|
||||||
|
|
||||||
// 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()}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to initialize modules:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example features to demonstrate the template
|
|
||||||
*/
|
|
||||||
private addExampleFeatures(): void {
|
|
||||||
// Add mobile-optimized styles
|
|
||||||
const mobileInfo = MobileUtils.detect();
|
|
||||||
const baseCss = `
|
|
||||||
.userscript-highlight {
|
|
||||||
background-color: yellow !important;
|
|
||||||
border: 2px solid red !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 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<number>('visitCount', 0) || 0) + 1;
|
|
||||||
Storage.set('visitCount', visitCount);
|
|
||||||
console.log(`📊 This is visit #${visitCount}`);
|
|
||||||
|
|
||||||
// Example: Add menu command (with mobile detection)
|
|
||||||
if (typeof GM_registerMenuCommand !== 'undefined') {
|
|
||||||
GM_registerMenuCommand('Show Visit Count', () => {
|
|
||||||
const count = Storage.get<number>('visitCount', 0);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a module
|
|
||||||
*/
|
|
||||||
protected registerModule(name: string, module: any): void {
|
|
||||||
this.modules.set(name, module);
|
|
||||||
this.emit('moduleLoaded', { name });
|
|
||||||
console.log(`📦 Module "${name}" loaded`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a registered module
|
|
||||||
*/
|
|
||||||
protected getModule<T = any>(name: string): T | undefined {
|
|
||||||
return this.modules.get(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if app is initialized
|
|
||||||
*/
|
|
||||||
public get ready(): boolean {
|
|
||||||
return this.isInitialized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the application
|
|
||||||
new App();
|
new App();
|
||||||
|
|
@ -1,251 +0,0 @@
|
||||||
import { DOMUtils } from '@/utils/dom';
|
|
||||||
import { EventEmitter } from '@/utils/events';
|
|
||||||
import { Storage } from '@/utils/storage';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example module events
|
|
||||||
*/
|
|
||||||
interface ExampleModuleEvents {
|
|
||||||
initialized: void;
|
|
||||||
actionPerformed: { action: string; timestamp: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example module to demonstrate the template structure
|
|
||||||
*
|
|
||||||
* This module shows how to:
|
|
||||||
* - Extend the EventEmitter for module communication
|
|
||||||
* - Use storage for persistent data
|
|
||||||
* - Manipulate DOM elements
|
|
||||||
* - Register menu commands
|
|
||||||
*/
|
|
||||||
export class ExampleModule extends EventEmitter<ExampleModuleEvents> {
|
|
||||||
private isInitialized = false;
|
|
||||||
private actionCount = 0;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the module
|
|
||||||
*/
|
|
||||||
async initialize(): Promise<void> {
|
|
||||||
try {
|
|
||||||
console.log('📦 Initializing ExampleModule...');
|
|
||||||
|
|
||||||
// Load persistent data
|
|
||||||
this.actionCount = Storage.get<number>('exampleModule.actionCount', 0) || 0;
|
|
||||||
|
|
||||||
// Wait for required DOM elements (example)
|
|
||||||
await this.waitForPageElements();
|
|
||||||
|
|
||||||
// Add custom styles
|
|
||||||
this.addModuleStyles();
|
|
||||||
|
|
||||||
// Register menu commands
|
|
||||||
this.registerMenuCommands();
|
|
||||||
|
|
||||||
// Set up event listeners
|
|
||||||
this.setupEventListeners();
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
this.emit('initialized', undefined);
|
|
||||||
|
|
||||||
console.log('✅ ExampleModule initialized successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ExampleModule initialization failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for required page elements
|
|
||||||
*/
|
|
||||||
private async waitForPageElements(): Promise<void> {
|
|
||||||
// Example: Wait for body to be available
|
|
||||||
await DOMUtils.waitForElement('body', 5000);
|
|
||||||
console.log('📄 Page body is ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add module-specific styles
|
|
||||||
*/
|
|
||||||
private addModuleStyles(): void {
|
|
||||||
DOMUtils.addStyles(
|
|
||||||
`
|
|
||||||
.example-module-button {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 9999;
|
|
||||||
padding: 10px 15px;
|
|
||||||
background: #007cba;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-module-button:hover {
|
|
||||||
background: #005a87;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-module-notification {
|
|
||||||
position: fixed;
|
|
||||||
top: 70px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 9998;
|
|
||||||
padding: 10px;
|
|
||||||
background: #28a745;
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-module-notification.show {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
'example-module-styles'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register UserScript menu commands
|
|
||||||
*/
|
|
||||||
private registerMenuCommands(): void {
|
|
||||||
if (typeof GM_registerMenuCommand !== 'undefined') {
|
|
||||||
GM_registerMenuCommand('🎯 Perform Example Action', () => {
|
|
||||||
this.performAction('menu-command');
|
|
||||||
});
|
|
||||||
|
|
||||||
GM_registerMenuCommand('📊 Show Module Stats', () => {
|
|
||||||
this.showStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
GM_registerMenuCommand('🧹 Reset Module Data', () => {
|
|
||||||
this.resetData();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up event listeners
|
|
||||||
*/
|
|
||||||
private setupEventListeners(): void {
|
|
||||||
// Add a floating button for demonstration
|
|
||||||
const button = DOMUtils.createElement('button', {
|
|
||||||
className: 'example-module-button',
|
|
||||||
textContent: '🎯 Example Action',
|
|
||||||
onclick: () => this.performAction('button-click'),
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(button);
|
|
||||||
|
|
||||||
// Listen to keyboard shortcuts
|
|
||||||
document.addEventListener('keydown', event => {
|
|
||||||
// Ctrl+Shift+E to perform action
|
|
||||||
if (event.ctrlKey && event.shiftKey && event.key === 'E') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.performAction('keyboard-shortcut');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform an example action
|
|
||||||
*/
|
|
||||||
public performAction(trigger: string): void {
|
|
||||||
this.actionCount++;
|
|
||||||
const timestamp = Date.now();
|
|
||||||
|
|
||||||
// Store the updated count
|
|
||||||
Storage.set('exampleModule.actionCount', this.actionCount);
|
|
||||||
|
|
||||||
// Emit event
|
|
||||||
this.emit('actionPerformed', { action: trigger, timestamp });
|
|
||||||
|
|
||||||
// Show notification
|
|
||||||
this.showNotification(`Action performed via ${trigger}! (Total: ${this.actionCount})`);
|
|
||||||
|
|
||||||
console.log(`🎯 Example action performed via ${trigger} (count: ${this.actionCount})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a temporary notification
|
|
||||||
*/
|
|
||||||
private showNotification(message: string, duration = 3000): void {
|
|
||||||
const notification = DOMUtils.createElement('div', {
|
|
||||||
className: 'example-module-notification',
|
|
||||||
textContent: message,
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
// Trigger animation
|
|
||||||
setTimeout(() => notification.classList.add('show'), 10);
|
|
||||||
|
|
||||||
// Remove after duration
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.classList.remove('show');
|
|
||||||
setTimeout(() => DOMUtils.removeElement(notification), 300);
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show module statistics
|
|
||||||
*/
|
|
||||||
private showStats(): void {
|
|
||||||
const stats = {
|
|
||||||
initialized: this.isInitialized,
|
|
||||||
actionCount: this.actionCount,
|
|
||||||
lastAction: Storage.get<number>('exampleModule.lastActionTime', 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
const message = [
|
|
||||||
'Example Module Statistics:',
|
|
||||||
`• Initialized: ${stats.initialized ? 'Yes' : 'No'}`,
|
|
||||||
`• Actions performed: ${stats.actionCount}`,
|
|
||||||
`• Last action: ${stats.lastAction ? new Date(stats.lastAction).toLocaleString() : 'Never'}`,
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
if (typeof GM_notification !== 'undefined') {
|
|
||||||
GM_notification(message, 'Module Stats');
|
|
||||||
} else {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset module data
|
|
||||||
*/
|
|
||||||
private resetData(): void {
|
|
||||||
if (confirm('Are you sure you want to reset all module data?')) {
|
|
||||||
Storage.remove('exampleModule.actionCount');
|
|
||||||
Storage.remove('exampleModule.lastActionTime');
|
|
||||||
this.actionCount = 0;
|
|
||||||
console.log('🧹 Example module data reset');
|
|
||||||
this.showNotification('Module data reset!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get module status
|
|
||||||
*/
|
|
||||||
public get initialized(): boolean {
|
|
||||||
return this.isInitialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get action count
|
|
||||||
*/
|
|
||||||
public get totalActions(): number {
|
|
||||||
return this.actionCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,276 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
99
src/types/userscript.d.ts
vendored
99
src/types/userscript.d.ts
vendored
|
|
@ -1,99 +0,0 @@
|
||||||
/**
|
|
||||||
* TypeScript definitions for UserScript environment
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
/**
|
|
||||||
* Greasemonkey/Tampermonkey API
|
|
||||||
*/
|
|
||||||
function GM_setValue(key: string, value: any): void;
|
|
||||||
function GM_getValue(key: string, defaultValue?: any): any;
|
|
||||||
function GM_deleteValue(key: string): void;
|
|
||||||
function GM_listValues(): string[];
|
|
||||||
|
|
||||||
function GM_log(message: string): void;
|
|
||||||
function GM_notification(
|
|
||||||
text: string,
|
|
||||||
title?: string,
|
|
||||||
image?: string,
|
|
||||||
onclick?: () => void
|
|
||||||
): void;
|
|
||||||
|
|
||||||
function GM_openInTab(
|
|
||||||
url: string,
|
|
||||||
options?: { active?: boolean; insert?: boolean; setParent?: boolean }
|
|
||||||
): void;
|
|
||||||
function GM_registerMenuCommand(name: string, fn: () => void, accessKey?: string): number;
|
|
||||||
function GM_unregisterMenuCommand(menuCmdId: number): void;
|
|
||||||
|
|
||||||
function GM_xmlhttpRequest(details: GM_XHRDetails): GM_XHRResponse;
|
|
||||||
|
|
||||||
interface GM_XHRDetails {
|
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
||||||
url: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
data?: string | FormData | Blob;
|
|
||||||
binary?: boolean;
|
|
||||||
timeout?: number;
|
|
||||||
context?: any;
|
|
||||||
responseType?: 'text' | 'json' | 'blob' | 'arraybuffer' | 'document';
|
|
||||||
overrideMimeType?: string;
|
|
||||||
anonymous?: boolean;
|
|
||||||
fetch?: boolean;
|
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
onload?: (response: GM_XHRResponse) => void;
|
|
||||||
onerror?: (response: GM_XHRResponse) => void;
|
|
||||||
onabort?: () => void;
|
|
||||||
ontimeout?: () => void;
|
|
||||||
onprogress?: (response: GM_XHRProgressResponse) => void;
|
|
||||||
onreadystatechange?: (response: GM_XHRResponse) => void;
|
|
||||||
onloadstart?: (response: GM_XHRResponse) => void;
|
|
||||||
onloadend?: (response: GM_XHRResponse) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GM_XHRResponse {
|
|
||||||
readyState: number;
|
|
||||||
responseHeaders: string;
|
|
||||||
responseText: string;
|
|
||||||
response: any;
|
|
||||||
status: number;
|
|
||||||
statusText: string;
|
|
||||||
finalUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GM_XHRProgressResponse extends GM_XHRResponse {
|
|
||||||
lengthComputable: boolean;
|
|
||||||
loaded: number;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserScript Info
|
|
||||||
*/
|
|
||||||
interface GM_Info {
|
|
||||||
script: {
|
|
||||||
description: string;
|
|
||||||
excludes: string[];
|
|
||||||
includes: string[];
|
|
||||||
matches: string[];
|
|
||||||
name: string;
|
|
||||||
namespace: string;
|
|
||||||
resources: Record<string, string>;
|
|
||||||
runAt: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
scriptMetaStr: string;
|
|
||||||
scriptWillUpdate: boolean;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GM_info: GM_Info;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UnsafeWindow for accessing page's global scope
|
|
||||||
*/
|
|
||||||
const unsafeWindow: Window & typeof globalThis;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {};
|
|
||||||
126
src/utils/dom.ts
126
src/utils/dom.ts
|
|
@ -1,126 +0,0 @@
|
||||||
/**
|
|
||||||
* DOM utility functions for UserScript development
|
|
||||||
*/
|
|
||||||
export class DOMUtils {
|
|
||||||
/**
|
|
||||||
* Wait for element to appear in DOM
|
|
||||||
*/
|
|
||||||
static waitForElement(
|
|
||||||
selector: string,
|
|
||||||
timeout = 10000,
|
|
||||||
root: Document | Element = document
|
|
||||||
): Promise<Element> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const element = root.querySelector(selector);
|
|
||||||
if (element) {
|
|
||||||
resolve(element);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new MutationObserver(mutations => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
const nodes = Array.from(mutation.addedNodes);
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const element =
|
|
||||||
(node as Element).querySelector?.(selector) || (node as Element).matches?.(selector)
|
|
||||||
? (node as Element)
|
|
||||||
: null;
|
|
||||||
if (element) {
|
|
||||||
observer.disconnect();
|
|
||||||
resolve(element);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(root, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
observer.disconnect();
|
|
||||||
reject(new Error(`Element "${selector}" not found within ${timeout}ms`));
|
|
||||||
}, timeout);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for multiple elements
|
|
||||||
*/
|
|
||||||
static waitForElements(
|
|
||||||
selectors: string[],
|
|
||||||
timeout = 10000,
|
|
||||||
root: Document | Element = document
|
|
||||||
): Promise<Element[]> {
|
|
||||||
return Promise.all(selectors.map(selector => this.waitForElement(selector, timeout, root)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create element with attributes and content
|
|
||||||
*/
|
|
||||||
static createElement<K extends keyof HTMLElementTagNameMap>(
|
|
||||||
tagName: K,
|
|
||||||
attributes: Partial<HTMLElementTagNameMap[K]> = {},
|
|
||||||
content?: string | Node
|
|
||||||
): HTMLElementTagNameMap[K] {
|
|
||||||
const element = document.createElement(tagName);
|
|
||||||
|
|
||||||
Object.assign(element, attributes);
|
|
||||||
|
|
||||||
if (content !== undefined) {
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
element.textContent = content;
|
|
||||||
} else {
|
|
||||||
element.appendChild(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add CSS styles to page
|
|
||||||
*/
|
|
||||||
static addStyles(css: string, id?: string): HTMLStyleElement {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = css;
|
|
||||||
if (id) {
|
|
||||||
style.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
(document.head || document.documentElement).appendChild(style);
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove element safely
|
|
||||||
*/
|
|
||||||
static removeElement(element: Element | string): boolean {
|
|
||||||
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
||||||
if (el && el.parentNode) {
|
|
||||||
el.parentNode.removeChild(el);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if element is visible
|
|
||||||
*/
|
|
||||||
static isVisible(element: Element): boolean {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
const style = window.getComputedStyle(element);
|
|
||||||
|
|
||||||
return (
|
|
||||||
rect.width > 0 &&
|
|
||||||
rect.height > 0 &&
|
|
||||||
style.visibility !== 'hidden' &&
|
|
||||||
style.display !== 'none' &&
|
|
||||||
style.opacity !== '0'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
/**
|
|
||||||
* Type-safe event emitter for UserScript modules
|
|
||||||
*/
|
|
||||||
export interface EventMap {
|
|
||||||
[event: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EventEmitter<T extends EventMap = EventMap> {
|
|
||||||
private listeners: { [K in keyof T]?: Array<(data: T[K]) => void> } = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add event listener
|
|
||||||
*/
|
|
||||||
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
|
|
||||||
if (!this.listeners[event]) {
|
|
||||||
this.listeners[event] = [];
|
|
||||||
}
|
|
||||||
this.listeners[event]!.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add one-time event listener
|
|
||||||
*/
|
|
||||||
once<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
|
|
||||||
const onceListener = (data: T[K]) => {
|
|
||||||
listener(data);
|
|
||||||
this.off(event, onceListener);
|
|
||||||
};
|
|
||||||
this.on(event, onceListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove event listener
|
|
||||||
*/
|
|
||||||
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
|
|
||||||
const eventListeners = this.listeners[event];
|
|
||||||
if (eventListeners) {
|
|
||||||
const index = eventListeners.indexOf(listener);
|
|
||||||
if (index > -1) {
|
|
||||||
eventListeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit event
|
|
||||||
*/
|
|
||||||
emit<K extends keyof T>(event: K, data: T[K]): void {
|
|
||||||
const eventListeners = this.listeners[event];
|
|
||||||
if (eventListeners) {
|
|
||||||
eventListeners.forEach(listener => {
|
|
||||||
try {
|
|
||||||
listener(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error in event listener for "${String(event)}":`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all listeners for an event
|
|
||||||
*/
|
|
||||||
removeAllListeners<K extends keyof T>(event?: K): void {
|
|
||||||
if (event) {
|
|
||||||
delete this.listeners[event];
|
|
||||||
} else {
|
|
||||||
this.listeners = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get listener count for an event
|
|
||||||
*/
|
|
||||||
listenerCount<K extends keyof T>(event: K): number {
|
|
||||||
return this.listeners[event]?.length || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
/**
|
|
||||||
* Storage utility for UserScript persistent data
|
|
||||||
*/
|
|
||||||
export class Storage {
|
|
||||||
/**
|
|
||||||
* Set a value in persistent storage
|
|
||||||
*/
|
|
||||||
static set<T>(key: string, value: T): void {
|
|
||||||
try {
|
|
||||||
const serialized = JSON.stringify(value);
|
|
||||||
GM_setValue(key, serialized);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to store value for key "${key}":`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a value from persistent storage
|
|
||||||
*/
|
|
||||||
static get<T>(key: string, defaultValue?: T): T | undefined {
|
|
||||||
try {
|
|
||||||
const stored = GM_getValue(key);
|
|
||||||
if (stored === undefined) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
return JSON.parse(stored) as T;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to retrieve value for key "${key}":`, error);
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a value from persistent storage
|
|
||||||
*/
|
|
||||||
static remove(key: string): void {
|
|
||||||
GM_deleteValue(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a key exists in storage
|
|
||||||
*/
|
|
||||||
static has(key: string): boolean {
|
|
||||||
return GM_getValue(key) !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all keys from storage
|
|
||||||
*/
|
|
||||||
static getAllKeys(): string[] {
|
|
||||||
return GM_listValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all storage (use with caution!)
|
|
||||||
*/
|
|
||||||
static clear(): void {
|
|
||||||
const keys = GM_listValues();
|
|
||||||
keys.forEach(key => GM_deleteValue(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +1,15 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import config from '../header.config.json';
|
import pkg from "../package.json";
|
||||||
import pkg from '../package.json';
|
import config from "../header.config.json";
|
||||||
|
|
||||||
// Check which file exists to determine the environment
|
const targetFile = "./dist/" + pkg.name + ".user.js";
|
||||||
const prodFile = `./dist/${pkg.name}.user.js`;
|
|
||||||
const devFile = `./dist/${pkg.name}.dev.user.js`;
|
|
||||||
|
|
||||||
let targetFile: string;
|
|
||||||
let environment: 'production' | 'development';
|
|
||||||
|
|
||||||
if (fs.existsSync(prodFile)) {
|
|
||||||
targetFile = prodFile;
|
|
||||||
environment = 'production';
|
|
||||||
} else if (fs.existsSync(devFile)) {
|
|
||||||
targetFile = devFile;
|
|
||||||
environment = 'development';
|
|
||||||
} else {
|
|
||||||
console.error('❌ No UserScript file found in dist/');
|
|
||||||
console.error(`Expected either: ${prodFile} or ${devFile}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🔧 Building UserScript header for ${environment} (${targetFile})`);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends header
|
* Appends header
|
||||||
* @param header
|
* @param header
|
||||||
*/
|
*/
|
||||||
function appendHeader(header: string) {
|
function appendHeader(header: string) {
|
||||||
fs.readFile(targetFile, 'utf8', (err: NodeJS.ErrnoException | null, data: string) => {
|
fs.readFile(targetFile, "utf8", (err: NodeJS.ErrnoException | null, data: string) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +28,7 @@ function appendHeader(header: string) {
|
||||||
*/
|
*/
|
||||||
async function buildBase64UrlFromFile(filePath: string): Promise<string> {
|
async function buildBase64UrlFromFile(filePath: string): Promise<string> {
|
||||||
const file = await fs.promises.readFile(filePath);
|
const file = await fs.promises.readFile(filePath);
|
||||||
return 'data:image/png;base64,' + file.toString('base64');
|
return "data:image/png;base64," + file.toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,7 +38,7 @@ async function buildBase64UrlFromFile(filePath: string): Promise<string> {
|
||||||
* @returns multiple entries
|
* @returns multiple entries
|
||||||
*/
|
*/
|
||||||
function generateMultipleEntries(type: string, array: string[]): string {
|
function generateMultipleEntries(type: string, array: string[]): string {
|
||||||
let result: string = '';
|
let result: string = "";
|
||||||
if (array) {
|
if (array) {
|
||||||
array.forEach((item: string) => {
|
array.forEach((item: string) => {
|
||||||
result += `// ${type} ${item}\n`;
|
result += `// ${type} ${item}\n`;
|
||||||
|
|
@ -72,43 +53,28 @@ function generateMultipleEntries(type: string, array: string[]): string {
|
||||||
* @returns string without empty lines
|
* @returns string without empty lines
|
||||||
*/
|
*/
|
||||||
function removeEmptyLinesFromString(string: string): string {
|
function removeEmptyLinesFromString(string: string): string {
|
||||||
return string.replace(/\n\s*\n/g, '\n');
|
return string.replace(/\n\s*\n/g, "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates user script header
|
* Generates user script header
|
||||||
*/
|
*/
|
||||||
async function generateUserScriptHeader() {
|
async function generateUserScriptHeader() {
|
||||||
// Determine environment from NODE_ENV or default to production
|
const excludes = generateMultipleEntries("@exclude", config.excludes);
|
||||||
const environment = process.env.NODE_ENV === 'development' ? 'development' : 'production';
|
const requires = generateMultipleEntries("@require", config.requires);
|
||||||
|
const resources = generateMultipleEntries("@resource", config.resources);
|
||||||
// Get environment-specific config or fallback to empty arrays
|
const connecters = generateMultipleEntries("@connect", config.connecters);
|
||||||
const envConfig = config.environments?.[environment] || {
|
const grants = generateMultipleEntries("@grant", config.grants);
|
||||||
includes: [],
|
const matches = generateMultipleEntries("@match", config.matches);
|
||||||
excludes: [],
|
const includes = generateMultipleEntries("@match", config.includes);
|
||||||
grants: [],
|
const antifeatures = generateMultipleEntries("@antifeature", config.antifeatures);
|
||||||
};
|
|
||||||
|
|
||||||
const excludes = generateMultipleEntries('@exclude', envConfig.excludes);
|
|
||||||
const requires = generateMultipleEntries('@require', config.requires);
|
|
||||||
const resources = generateMultipleEntries('@resource', config.resources);
|
|
||||||
const connecters = generateMultipleEntries('@connect', config.connecters);
|
|
||||||
const grants = generateMultipleEntries('@grant', envConfig.grants);
|
|
||||||
|
|
||||||
// Use environment-specific includes as matches, fallback to root matches if available
|
|
||||||
const allMatches = envConfig.includes.length > 0 ? envConfig.includes : config.matches;
|
|
||||||
const matches = generateMultipleEntries('@match', allMatches);
|
|
||||||
|
|
||||||
// No includes needed as we use matches
|
|
||||||
const includes = '';
|
|
||||||
const antifeatures = generateMultipleEntries('@antifeature', config.antifeatures);
|
|
||||||
const base64url = await buildBase64UrlFromFile(config.iconUrl);
|
const base64url = await buildBase64UrlFromFile(config.iconUrl);
|
||||||
|
|
||||||
let noframes = '';
|
let noframes = "";
|
||||||
let matchAllFrames = '';
|
let matchAllFrames = "";
|
||||||
let updateUrl = '';
|
let updateUrl = "";
|
||||||
let downloadUrl = '';
|
let downloadUrl = "";
|
||||||
let supportUrl = '';
|
let supportUrl = "";
|
||||||
|
|
||||||
if (config.noframes) {
|
if (config.noframes) {
|
||||||
noframes = `// @noframes\n`;
|
noframes = `// @noframes\n`;
|
||||||
|
|
@ -116,13 +82,13 @@ async function generateUserScriptHeader() {
|
||||||
if (config.matchAllFrames) {
|
if (config.matchAllFrames) {
|
||||||
matchAllFrames = `// @matchAllFrames\n`;
|
matchAllFrames = `// @matchAllFrames\n`;
|
||||||
}
|
}
|
||||||
if (config.updateUrl !== '') {
|
if (config.updateUrl !== "") {
|
||||||
updateUrl = `// @updateURL ${config.updateUrl}\n`;
|
updateUrl = `// @updateURL ${config.updateUrl}\n`;
|
||||||
}
|
}
|
||||||
if (config.downloadUrl !== '') {
|
if (config.downloadUrl !== "") {
|
||||||
downloadUrl = `// @downloadURL ${config.downloadUrl}\n`;
|
downloadUrl = `// @downloadURL ${config.downloadUrl}\n`;
|
||||||
}
|
}
|
||||||
if (config.supportUrl !== '') {
|
if (config.supportUrl !== "") {
|
||||||
supportUrl = `// @supportURL ${config.supportUrl}\n`;
|
supportUrl = `// @supportURL ${config.supportUrl}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +117,7 @@ ${matchAllFrames}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
header = removeEmptyLinesFromString(header);
|
header = removeEmptyLinesFromString(header);
|
||||||
header += '\n';
|
header += "\n";
|
||||||
appendHeader(header);
|
appendHeader(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,16 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"exactOptionalPropertyTypes": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ES2020",
|
"target": "ES6",
|
||||||
"lib": ["ES2020", "DOM"],
|
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"baseUrl": "./src",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["*"],
|
"undici-types": ["./node_modules/undici/types/index.d.ts"]
|
||||||
"@/types/*": ["types/*"],
|
|
||||||
"@/utils/*": ["utils/*"],
|
|
||||||
"@/core/*": ["core/*"],
|
|
||||||
"undici-types": ["../node_modules/undici/types/index.d.ts"]
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "tools/**/*.js"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,21 @@
|
||||||
import { resolve } from "path";
|
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import { resolve } from "path";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import pkgjsn from "./package.json";
|
import pkgjsn from "./package.json";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig({
|
||||||
const isDev = mode === 'development';
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
return {
|
input: resolve(__dirname, "src/index.ts"),
|
||||||
build: {
|
output: {
|
||||||
rollupOptions: {
|
entryFileNames: `${pkgjsn.name}.user.js`,
|
||||||
input: resolve(__dirname, "src/index.ts"),
|
dir: resolve(__dirname, "dist"),
|
||||||
output: {
|
|
||||||
entryFileNames: `${pkgjsn.name}${isDev ? '.dev' : ''}.user.js`,
|
|
||||||
dir: resolve(__dirname, "dist"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sourcemap: isDev ? "inline" : false,
|
|
||||||
minify: isDev ? false : 'terser',
|
|
||||||
},
|
|
||||||
plugins: [tsconfigPaths()],
|
|
||||||
resolve: {
|
|
||||||
extensions: [".tsx", ".ts", ".js"],
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, 'src'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
sourcemap: "inline", // Equivalent to 'inline-source-map'
|
||||||
__DEV__: isDev,
|
},
|
||||||
__VERSION__: JSON.stringify(pkgjsn.version),
|
plugins: [tsconfigPaths()],
|
||||||
},
|
resolve: {
|
||||||
};
|
extensions: [".tsx", ".ts", ".js"],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue