mirror of
https://github.com/JosunLP/UserScriptProjectTemplate.git
synced 2025-10-14 17:10:11 +00:00
feat: Enhance UserScript structure and functionality
- Updated package.json to include new scripts for development, production builds, linting, formatting, and cleaning. - Added ESLint and Prettier for code quality and formatting. - Refactored main application class to extend EventEmitter and manage modules. - Introduced ExampleModule to demonstrate module structure and functionality. - Created utility classes for DOM manipulation, event handling, and persistent storage. - Added TypeScript definitions for UserScript environment. - Improved TypeScript configuration with stricter checks and path aliases. - Updated Vite configuration to handle development and production builds more effectively. - Enhanced user script header generation to support environment-specific configurations.
This commit is contained in:
parent
8089771d41
commit
88aeab8f29
17 changed files with 4064 additions and 595 deletions
156
src/index.ts
156
src/index.ts
|
@ -1,15 +1,147 @@
|
|||
/**
|
||||
* App
|
||||
*/
|
||||
class App {
|
||||
import { ExampleModule } from '@/modules/example';
|
||||
import { DOMUtils } from '@/utils/dom';
|
||||
import { EventEmitter } from '@/utils/events';
|
||||
import { Storage } from '@/utils/storage';
|
||||
|
||||
constructor() {
|
||||
this.main();
|
||||
}
|
||||
|
||||
private main() {
|
||||
console.log('Hello World!');
|
||||
}
|
||||
/**
|
||||
* Application events interface
|
||||
*/
|
||||
interface AppEvents {
|
||||
ready: void;
|
||||
error: Error;
|
||||
moduleLoaded: { name: string };
|
||||
}
|
||||
|
||||
new App();
|
||||
/**
|
||||
* 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!');
|
||||
|
||||
// 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();
|
||||
|
||||
// Register the module
|
||||
this.registerModule('example', exampleModule);
|
||||
|
||||
// Listen to 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 {
|
||||
// Example: Add custom styles
|
||||
DOMUtils.addStyles(
|
||||
`
|
||||
.userscript-highlight {
|
||||
background-color: yellow !important;
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
`,
|
||||
'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
|
||||
if (typeof GM_registerMenuCommand !== 'undefined') {
|
||||
GM_registerMenuCommand('Show Visit Count', () => {
|
||||
const count = Storage.get<number>('visitCount', 0);
|
||||
alert(`You have visited this page ${count} times!`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
|
251
src/modules/example.ts
Normal file
251
src/modules/example.ts
Normal file
|
@ -0,0 +1,251 @@
|
|||
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;
|
||||
}
|
||||
}
|
99
src/types/userscript.d.ts
vendored
Normal file
99
src/types/userscript.d.ts
vendored
Normal file
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* 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
Normal file
126
src/utils/dom.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* 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'
|
||||
);
|
||||
}
|
||||
}
|
78
src/utils/events.ts
Normal file
78
src/utils/events.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
61
src/utils/storage.ts
Normal file
61
src/utils/storage.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue