├── assets └── screenshot.png ├── CHANGELOG.md ├── src ├── utils │ ├── base64-utils.js │ ├── lorem-utils.js │ ├── password-utils.js │ ├── unit-converter-utils.js │ └── api-formatter-utils.js ├── renderer │ └── js │ │ └── tools │ │ ├── case-converter.js │ │ ├── base64-converter.js │ │ ├── jwt-decoder.js │ │ ├── json-viewer.js │ │ ├── json-to-ts.js │ │ ├── number-base.js │ │ ├── timestamp.js │ │ ├── url-encoder.js │ │ ├── date-difference.js │ │ ├── unit-converter.js │ │ ├── uuid-generator.js │ │ ├── sql-formatter.js │ │ ├── image-converter.js │ │ ├── csv-json.js │ │ ├── lorem-generator.js │ │ ├── qr-generator.js │ │ ├── cron-calculator.js │ │ └── hash-generator.js └── main.js ├── tests ├── tools │ ├── lorem-generator-integration.test.js │ ├── base64-converter.test.js │ ├── lorem-generator.test.js │ └── password-generator.test.js └── setup.js ├── package.json ├── .github └── copilot-instructions.md ├── .gitignore └── README.md /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/me-shaon/devtools/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | -------------------------------------------------------------------------------- /src/utils/base64-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base64 utilities for testing - extracted from Base64Converter 3 | */ 4 | 5 | class Base64Utils { 6 | static encodeText(text) { 7 | if (!text || !text.trim()) { 8 | throw new Error('Please enter text to encode.'); 9 | } 10 | 11 | try { 12 | return btoa(unescape(encodeURIComponent(text))); 13 | } catch (error) { 14 | throw new Error('Error encoding text: ' + error.message); 15 | } 16 | } 17 | 18 | static decodeText(base64) { 19 | if (!base64 || !base64.trim()) { 20 | throw new Error('Please enter Base64 to decode.'); 21 | } 22 | 23 | if (!this.isValidBase64(base64.trim())) { 24 | throw new Error('Invalid Base64 format'); 25 | } 26 | 27 | try { 28 | return decodeURIComponent(escape(atob(base64.trim()))); 29 | } catch (error) { 30 | throw new Error( 31 | 'Error decoding Base64: Invalid format or corrupted data' 32 | ); 33 | } 34 | } 35 | 36 | static isValidBase64(str) { 37 | if (!str || str.trim() === '') return false; 38 | 39 | try { 40 | return btoa(atob(str)) === str; 41 | } catch (err) { 42 | return false; 43 | } 44 | } 45 | } 46 | 47 | module.exports = Base64Utils; 48 | -------------------------------------------------------------------------------- /tests/tools/lorem-generator-integration.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Integration test for Lorem Generator functionality 3 | * Focuses on core functionality without complex DOM interactions 4 | */ 5 | 6 | // Import the lorem generator by requiring the utils 7 | const path = require('path'); 8 | const LoremUtils = require('../../src/utils/lorem-utils.js'); 9 | 10 | describe('LoremGenerator Functionality', () => { 11 | test('generates paragraphs using utils', () => { 12 | const result = LoremUtils.generateParagraphs(2); 13 | expect(result).toBeTruthy(); 14 | expect(typeof result).toBe('string'); 15 | expect(result.includes('lorem')).toBeTruthy(); 16 | // Should have 2 paragraphs separated by double newlines 17 | expect(result.split('\n\n')).toHaveLength(2); 18 | }); 19 | 20 | test('generates sentences using utils', () => { 21 | const result = LoremUtils.generateSentences(3); 22 | expect(result).toBeTruthy(); 23 | expect(typeof result).toBe('string'); 24 | // Should end with punctuation 25 | expect(/[.!?]$/.test(result)).toBeTruthy(); 26 | }); 27 | 28 | test('generates words using utils', () => { 29 | const result = LoremUtils.generateWords(10); 30 | expect(result).toBeTruthy(); 31 | expect(typeof result).toBe('string'); 32 | const words = result.split(' '); 33 | expect(words).toHaveLength(10); 34 | }); 35 | 36 | test('handles edge cases', () => { 37 | expect(() => LoremUtils.generateParagraphs(0)).toThrow(); 38 | expect(() => LoremUtils.generateSentences(0)).toThrow(); 39 | expect(() => LoremUtils.generateWords(0)).toThrow(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Jest setup file for DevTools Desktop 3 | * This file runs before each test file 4 | */ 5 | 6 | // Mock DOM elements and browser APIs that our tools might use 7 | global.btoa = (str) => Buffer.from(str, 'binary').toString('base64'); 8 | global.atob = (str) => Buffer.from(str, 'base64').toString('binary'); 9 | 10 | // Mock navigator.clipboard 11 | Object.defineProperty(global.navigator, 'clipboard', { 12 | value: { 13 | writeText: jest.fn(() => Promise.resolve()), 14 | readText: jest.fn(() => Promise.resolve('')), 15 | }, 16 | writable: true, 17 | }); 18 | 19 | // Mock window.app (Electron main process communication) 20 | global.window = global.window || {}; 21 | global.window.app = { 22 | showMessage: jest.fn(), 23 | }; 24 | 25 | // Mock crypto for UUID generation 26 | global.crypto = { 27 | getRandomValues: jest.fn((arr) => { 28 | for (let i = 0; i < arr.length; i++) { 29 | arr[i] = Math.floor(Math.random() * 256); 30 | } 31 | return arr; 32 | }), 33 | }; 34 | 35 | // Mock document methods 36 | global.document = global.document || {}; 37 | global.document.getElementById = jest.fn(); 38 | global.document.querySelector = jest.fn(); 39 | global.document.querySelectorAll = jest.fn(); 40 | global.document.createElement = jest.fn(); 41 | 42 | // Helper function to create mock elements 43 | global.createMockElement = (tag = 'div', props = {}) => { 44 | const element = { 45 | tagName: tag.toUpperCase(), 46 | value: '', 47 | textContent: '', 48 | innerHTML: '', 49 | addEventListener: jest.fn(), 50 | removeEventListener: jest.fn(), 51 | click: jest.fn(), 52 | select: jest.fn(), 53 | setSelectionRange: jest.fn(), 54 | classList: { 55 | add: jest.fn(), 56 | remove: jest.fn(), 57 | contains: jest.fn(), 58 | toggle: jest.fn(), 59 | }, 60 | dataset: {}, 61 | ...props, 62 | }; 63 | 64 | return element; 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-tools-desktop", 3 | "version": "1.0.0", 4 | "description": "Desktop version of dev-tool.dev - Ultimate Developer Toolkit", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "dev": "electron . --dev", 9 | "build": "electron-builder", 10 | "pack": "electron-builder --dir", 11 | "dist": "electron-builder --publish=never", 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "test:coverage": "jest --coverage", 15 | "security-check": "./scripts/security-check.sh", 16 | "pre-commit": "npm run security-check" 17 | }, 18 | "keywords": [ 19 | "electron", 20 | "developer-tools", 21 | "utilities", 22 | "json", 23 | "base64", 24 | "uuid", 25 | "converter" 26 | ], 27 | "author": "Dev Tools Desktop", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "electron": "^28.0.0", 31 | "electron-builder": "^24.0.0", 32 | "jest": "^29.7.0", 33 | "jest-environment-jsdom": "^29.7.0" 34 | }, 35 | "dependencies": { 36 | "crypto-js": "^4.2.0", 37 | "jsonwebtoken": "^9.0.2", 38 | "marked": "^12.0.0", 39 | "node-cron": "^3.0.3", 40 | "qrcode": "^1.5.3", 41 | "uuid": "^9.0.1" 42 | }, 43 | "build": { 44 | "appId": "com.devtools.desktop", 45 | "productName": "Dev Tools Desktop", 46 | "directories": { 47 | "output": "dist" 48 | }, 49 | "files": [ 50 | "src/**/*", 51 | "node_modules/**/*" 52 | ], 53 | "mac": { 54 | "category": "public.app-category.developer-tools" 55 | }, 56 | "win": { 57 | "target": "nsis" 58 | }, 59 | "linux": { 60 | "target": "AppImage" 61 | } 62 | }, 63 | "jest": { 64 | "testEnvironment": "jsdom", 65 | "collectCoverageFrom": [ 66 | "src/renderer/js/tools/**/*.js", 67 | "!src/renderer/js/tools/**/*.test.js" 68 | ], 69 | "testMatch": [ 70 | "**/__tests__/**/*.test.js", 71 | "**/*.test.js" 72 | ], 73 | "setupFilesAfterEnv": [ 74 | "/tests/setup.js" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/tools/base64-converter.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test for Base64 utilities 3 | */ 4 | 5 | const Base64Utils = require('../../src/utils/base64-utils'); 6 | 7 | describe('Base64Utils', () => { 8 | describe('encodeText', () => { 9 | it('should encode text to base64', () => { 10 | const result = Base64Utils.encodeText('Hello World'); 11 | expect(result).toBe('SGVsbG8gV29ybGQ='); 12 | }); 13 | 14 | it('should throw error for empty input', () => { 15 | expect(() => Base64Utils.encodeText('')).toThrow( 16 | 'Please enter text to encode.' 17 | ); 18 | expect(() => Base64Utils.encodeText(' ')).toThrow( 19 | 'Please enter text to encode.' 20 | ); 21 | expect(() => Base64Utils.encodeText(null)).toThrow( 22 | 'Please enter text to encode.' 23 | ); 24 | }); 25 | 26 | it('should handle special characters correctly', () => { 27 | const result = Base64Utils.encodeText('Hello 🌟 World!'); 28 | expect(result).toBe('SGVsbG8g8J+MnyBXb3JsZCE='); 29 | }); 30 | 31 | it('should handle newlines and special characters', () => { 32 | const result = Base64Utils.encodeText('Hello\nWorld\t!'); 33 | expect(result).toBe('SGVsbG8KV29ybGQJIQ=='); 34 | }); 35 | }); 36 | 37 | describe('decodeText', () => { 38 | it('should decode base64 to text', () => { 39 | const result = Base64Utils.decodeText('SGVsbG8gV29ybGQ='); 40 | expect(result).toBe('Hello World'); 41 | }); 42 | 43 | it('should throw error for empty input', () => { 44 | expect(() => Base64Utils.decodeText('')).toThrow( 45 | 'Please enter Base64 to decode.' 46 | ); 47 | expect(() => Base64Utils.decodeText(' ')).toThrow( 48 | 'Please enter Base64 to decode.' 49 | ); 50 | expect(() => Base64Utils.decodeText(null)).toThrow( 51 | 'Please enter Base64 to decode.' 52 | ); 53 | }); 54 | 55 | it('should throw error for invalid base64', () => { 56 | expect(() => Base64Utils.decodeText('invalid-base64!@#')).toThrow( 57 | 'Invalid Base64 format' 58 | ); 59 | expect(() => Base64Utils.decodeText('SGVsbG8gV29ybGQ')).toThrow( 60 | 'Invalid Base64 format' 61 | ); // missing padding 62 | }); 63 | 64 | it('should decode special characters correctly', () => { 65 | const result = Base64Utils.decodeText('SGVsbG8g8J+MnyBXb3JsZCE='); 66 | expect(result).toBe('Hello 🌟 World!'); 67 | }); 68 | }); 69 | 70 | describe('isValidBase64', () => { 71 | it('should validate correct base64 strings', () => { 72 | expect(Base64Utils.isValidBase64('SGVsbG8gV29ybGQ=')).toBe(true); 73 | expect(Base64Utils.isValidBase64('dGVzdA==')).toBe(true); 74 | expect(Base64Utils.isValidBase64('SGVsbG8g8J+MnyBXb3JsZCE=')).toBe(true); 75 | }); 76 | 77 | it('should reject invalid base64 strings', () => { 78 | expect(Base64Utils.isValidBase64('invalid!@#')).toBe(false); 79 | expect(Base64Utils.isValidBase64('SGVsbG8gV29ybGQ')).toBe(false); // missing padding 80 | expect(Base64Utils.isValidBase64('')).toBe(false); 81 | }); 82 | 83 | it('should handle edge cases', () => { 84 | expect(Base64Utils.isValidBase64('QQ==')).toBe(true); // 'A' encoded 85 | expect(Base64Utils.isValidBase64('QUI=')).toBe(true); // 'AB' encoded 86 | expect(Base64Utils.isValidBase64('QUJD')).toBe(true); // 'ABC' encoded 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/renderer/js/tools/case-converter.js: -------------------------------------------------------------------------------- 1 | class CaseConverter { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const inputTextarea = document.getElementById('case-input'); 8 | const outputTextarea = document.getElementById('case-output'); 9 | const caseButtons = document.querySelectorAll('.case-btn'); 10 | 11 | if (caseButtons.length > 0) { 12 | caseButtons.forEach(button => { 13 | button.addEventListener('click', () => { 14 | const caseType = button.dataset.case; 15 | this.convertCase(caseType); 16 | }); 17 | }); 18 | } 19 | 20 | if (inputTextarea) { 21 | inputTextarea.addEventListener('input', () => { 22 | outputTextarea.value = ''; 23 | }); 24 | } 25 | } 26 | 27 | convertCase(caseType) { 28 | const input = document.getElementById('case-input').value; 29 | const output = document.getElementById('case-output'); 30 | 31 | if (!input.trim()) { 32 | window.app?.showMessage('Please enter text to convert.', 'error'); 33 | return; 34 | } 35 | 36 | let convertedText = ''; 37 | 38 | switch (caseType) { 39 | case 'camel': 40 | convertedText = this.toCamelCase(input); 41 | break; 42 | case 'pascal': 43 | convertedText = this.toPascalCase(input); 44 | break; 45 | case 'snake': 46 | convertedText = this.toSnakeCase(input); 47 | break; 48 | case 'kebab': 49 | convertedText = this.toKebabCase(input); 50 | break; 51 | case 'upper': 52 | convertedText = input.toUpperCase(); 53 | break; 54 | case 'lower': 55 | convertedText = input.toLowerCase(); 56 | break; 57 | case 'title': 58 | convertedText = this.toTitleCase(input); 59 | break; 60 | default: 61 | convertedText = input; 62 | } 63 | 64 | output.value = convertedText; 65 | window.app?.showMessage(`Text converted to ${caseType} case!`, 'success'); 66 | 67 | this.selectOutput(); 68 | } 69 | 70 | toCamelCase(str) { 71 | return str 72 | .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => { 73 | return index === 0 ? word.toLowerCase() : word.toUpperCase(); 74 | }) 75 | .replace(/\s+/g, '') 76 | .replace(/[^a-zA-Z0-9]/g, ''); 77 | } 78 | 79 | toPascalCase(str) { 80 | return str 81 | .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => { 82 | return word.toUpperCase(); 83 | }) 84 | .replace(/\s+/g, '') 85 | .replace(/[^a-zA-Z0-9]/g, ''); 86 | } 87 | 88 | toSnakeCase(str) { 89 | return str 90 | .replace(/\W+/g, ' ') 91 | .split(/ |\B(?=[A-Z])/) 92 | .map(word => word.toLowerCase()) 93 | .join('_') 94 | .replace(/_{2,}/g, '_') 95 | .replace(/^_|_$/g, ''); 96 | } 97 | 98 | toKebabCase(str) { 99 | return str 100 | .replace(/\W+/g, ' ') 101 | .split(/ |\B(?=[A-Z])/) 102 | .map(word => word.toLowerCase()) 103 | .join('-') 104 | .replace(/-{2,}/g, '-') 105 | .replace(/^-|-$/g, ''); 106 | } 107 | 108 | toTitleCase(str) { 109 | return str.replace(/\w\S*/g, (txt) => { 110 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 111 | }); 112 | } 113 | 114 | selectOutput() { 115 | const output = document.getElementById('case-output'); 116 | setTimeout(() => { 117 | output.select(); 118 | output.setSelectionRange(0, 99999); 119 | }, 100); 120 | } 121 | } 122 | 123 | window.CaseConverter = new CaseConverter(); -------------------------------------------------------------------------------- /src/renderer/js/tools/base64-converter.js: -------------------------------------------------------------------------------- 1 | class Base64Converter { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const container = document.getElementById('base64-converter'); 8 | const tabButtons = container.querySelectorAll('.tab-btn'); 9 | const encodeBtn = document.getElementById('encode-btn'); 10 | const decodeBtn = document.getElementById('decode-btn'); 11 | 12 | tabButtons.forEach(button => { 13 | button.addEventListener('click', () => { 14 | this.switchTab(button.dataset.tab); 15 | }); 16 | }); 17 | 18 | if (encodeBtn) { 19 | encodeBtn.addEventListener('click', () => this.encodeToBase64()); 20 | } 21 | 22 | if (decodeBtn) { 23 | decodeBtn.addEventListener('click', () => this.decodeFromBase64()); 24 | } 25 | 26 | const textInputs = document.querySelectorAll('#text-to-encode, #base64-to-decode'); 27 | textInputs.forEach(input => { 28 | input.addEventListener('input', () => this.clearOutputs()); 29 | }); 30 | } 31 | 32 | switchTab(tabName) { 33 | const container = document.getElementById('base64-converter'); 34 | 35 | container.querySelectorAll('.tab-btn').forEach(btn => { 36 | btn.classList.remove('active'); 37 | }); 38 | container.querySelectorAll('.tab-content').forEach(content => { 39 | content.classList.remove('active'); 40 | }); 41 | 42 | container.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); 43 | document.getElementById(`${tabName}-tab`).classList.add('active'); 44 | 45 | this.clearOutputs(); 46 | } 47 | 48 | encodeToBase64() { 49 | const input = document.getElementById('text-to-encode').value; 50 | const output = document.getElementById('encoded-output'); 51 | 52 | if (!input.trim()) { 53 | window.app?.showMessage('Please enter text to encode.', 'error'); 54 | return; 55 | } 56 | 57 | try { 58 | const encoded = btoa(unescape(encodeURIComponent(input))); 59 | output.value = encoded; 60 | window.app?.showMessage('Text encoded to Base64!', 'success'); 61 | this.selectOutput('encoded-output'); 62 | } catch (error) { 63 | window.app?.showMessage('Error encoding text: ' + error.message, 'error'); 64 | output.value = ''; 65 | } 66 | } 67 | 68 | decodeFromBase64() { 69 | const input = document.getElementById('base64-to-decode').value; 70 | const output = document.getElementById('decoded-output'); 71 | 72 | if (!input.trim()) { 73 | window.app?.showMessage('Please enter Base64 to decode.', 'error'); 74 | return; 75 | } 76 | 77 | try { 78 | if (!this.isValidBase64(input.trim())) { 79 | throw new Error('Invalid Base64 format'); 80 | } 81 | 82 | const decoded = decodeURIComponent(escape(atob(input.trim()))); 83 | output.value = decoded; 84 | window.app?.showMessage('Base64 decoded successfully!', 'success'); 85 | this.selectOutput('decoded-output'); 86 | } catch (error) { 87 | window.app?.showMessage('Error decoding Base64: Invalid format or corrupted data', 'error'); 88 | output.value = ''; 89 | } 90 | } 91 | 92 | isValidBase64(str) { 93 | try { 94 | return btoa(atob(str)) === str; 95 | } catch (err) { 96 | return false; 97 | } 98 | } 99 | 100 | selectOutput(outputId) { 101 | const output = document.getElementById(outputId); 102 | setTimeout(() => { 103 | output.select(); 104 | output.setSelectionRange(0, 99999); 105 | }, 100); 106 | } 107 | 108 | clearOutputs() { 109 | document.getElementById('encoded-output').value = ''; 110 | document.getElementById('decoded-output').value = ''; 111 | } 112 | } 113 | 114 | window.Base64Converter = new Base64Converter(); -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # DevTools Desktop - AI Development Guide 2 | 3 | ## Project Architecture 4 | 5 | **Single-Page Electron App**: This is an offline-first desktop application built with Electron + vanilla JavaScript. No frameworks, no build steps for frontend code. 6 | 7 | ``` 8 | src/ 9 | ├── main.js # Electron main process (menu, window management) 10 | └── renderer/ 11 | ├── index.html # SPA with all tools in one file (1327 lines) 12 | ├── js/main.js # App router, tool switching, IPC communication 13 | └── js/tools/ # 22 self-contained tool modules 14 | ``` 15 | 16 | ## Critical Patterns 17 | 18 | ### Tool Module Structure 19 | 20 | Each tool follows this exact pattern - **always match this when adding new tools**: 21 | 22 | ```javascript 23 | class ToolName { 24 | constructor() { 25 | this.init(); 26 | } 27 | 28 | init() { 29 | // Set up event listeners for tool-specific buttons 30 | const btn = document.getElementById('tool-button'); 31 | if (btn) { 32 | btn.addEventListener('click', () => this.methodName()); 33 | } 34 | } 35 | } 36 | 37 | // Global instance registration 38 | window.ToolName = new ToolName(); 39 | ``` 40 | 41 | ### Tool Registration Workflow 42 | 43 | Adding a new tool requires **exactly 4 steps**: 44 | 45 | 1. **Create module**: `src/renderer/js/tools/new-tool.js` 46 | 2. **Add HTML section**: In `index.html` with class `tool-container` and matching ID 47 | 3. **Add navigation**: In `index.html` sidebar with `data-tool="new-tool"` 48 | 4. **Add menu item**: In `src/main.js` electron menu (if needed) 49 | 50 | ### Dual Environment Support 51 | 52 | **Critical**: All tools must work in both Electron and browser environments: 53 | 54 | ```javascript 55 | const isElectron = 56 | typeof require !== 'undefined' && 57 | window.process && 58 | window.process.type === 'renderer'; 59 | ``` 60 | 61 | Use `window.app?.showMessage()` for notifications (fails gracefully in browser). 62 | 63 | ### Error Handling Pattern 64 | 65 | **Standard error display** used across all tools: 66 | 67 | ```javascript 68 | showError(message) { 69 | const errorDiv = document.getElementById('tool-error'); 70 | if (errorDiv) { 71 | errorDiv.textContent = message; 72 | errorDiv.style.display = 'block'; 73 | } 74 | } 75 | 76 | clearErrors() { 77 | const errorDiv = document.getElementById('tool-error'); 78 | if (errorDiv) { 79 | errorDiv.style.display = 'none'; 80 | } 81 | } 82 | ``` 83 | 84 | ## Testing Architecture 85 | 86 | **Jest + Node.js environment** with extensive DOM mocking in `tests/setup.js`. Tests are split into: 87 | 88 | - **Unit tests**: `src/utils/*.js` modules (extracted logic) 89 | - **Integration tests**: Tool classes with mocked DOM 90 | - **Security tests**: Referenced but `scripts/security-check.sh` missing 91 | 92 | **Test file naming**: `toolname.test.js` and `toolname-integration.test.js` 93 | 94 | ## Development Commands 95 | 96 | ```bash 97 | npm start # Electron dev mode 98 | npm run dev # Electron with DevTools open 99 | npm test # Jest test suite 100 | npm run test:watch # Jest watch mode 101 | npm run dist # Build distribution packages 102 | ``` 103 | 104 | ## Dependencies & Constraints 105 | 106 | **Runtime deps**: `crypto-js`, `jsonwebtoken`, `marked`, `node-cron`, `qrcode`, `uuid` 107 | **No build pipeline**: Vanilla JS/CSS, direct file serving 108 | **No analytics**: Completely offline, no telemetry 109 | **Security-first**: GitGuardian integration, pre-commit hooks 110 | 111 | ## State Management 112 | 113 | **No global state** - each tool is isolated. App state limited to: 114 | 115 | - `currentTool` in DevToolsApp class 116 | - Tool-specific state in individual tool instances 117 | - Electron preferences via IPC (`src/main.js` handles file operations) 118 | 119 | ## Key Integration Points 120 | 121 | - **IPC**: `ipcRenderer.invoke('save-file')` for file operations 122 | - **Tool activation**: `switchTool()` method triggers tool-specific `init()` methods 123 | - **Menu navigation**: Electron menu items trigger IPC messages to switch tools 124 | - **Clipboard**: Used extensively but mocked in tests with `navigator.clipboard` 125 | 126 | Focus on self-contained, offline-capable tools that follow the established module pattern. 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Electron-specific ignores 133 | # Build output directories 134 | build/ 135 | dist/ 136 | out/ 137 | release/ 138 | 139 | # Electron Forge 140 | .makers/ 141 | 142 | # Electron Builder 143 | # https://www.electron.build/configuration/configuration#directories 144 | app-builds/ 145 | 146 | # Generated installers 147 | *.dmg 148 | *.pkg 149 | *.msi 150 | *.exe 151 | *.deb 152 | *.rpm 153 | *.snap 154 | *.appimage 155 | 156 | # macOS specific files 157 | .DS_Store 158 | .DS_Store? 159 | ._* 160 | .Spotlight-V100 161 | .Trashes 162 | ehthumbs.db 163 | Thumbs.db 164 | 165 | # Windows specific files 166 | *.lnk 167 | desktop.ini 168 | 169 | # Linux specific files 170 | *~ 171 | 172 | # Temporary files 173 | *.tmp 174 | *.temp 175 | *.swp 176 | *.swo 177 | *~ 178 | 179 | # IDEs and editors 180 | .vscode/ 181 | .idea/ 182 | *.swp 183 | *.swo 184 | *~ 185 | .project 186 | .metadata 187 | .classpath 188 | .c9/ 189 | *.launch 190 | .settings/ 191 | *.sublime-workspace 192 | *.sublime-project 193 | 194 | # OS generated files 195 | .DS_Store* 196 | Icon? 197 | 198 | # Thumbnails 199 | ._* 200 | 201 | # Files that might appear on external disk 202 | .Spotlight-V100 203 | .Trashes 204 | 205 | # Archives 206 | *.7z 207 | *.dmg 208 | *.gz 209 | *.iso 210 | *.jar 211 | *.rar 212 | *.tar 213 | *.zip 214 | 215 | # Electron specific 216 | # Main process files (if using webpack or similar bundlers) 217 | app/ 218 | main.js.map 219 | renderer.js.map 220 | 221 | # Auto-generated files 222 | auto-updater/ 223 | 224 | # Release files 225 | releases/ 226 | 227 | # Test coverage 228 | coverage/ 229 | 230 | # Storybook build outputs 231 | .out 232 | .storybook-out 233 | storybook-static 234 | 235 | # Temporary folders 236 | tmp/ 237 | temp/ -------------------------------------------------------------------------------- /src/renderer/js/tools/jwt-decoder.js: -------------------------------------------------------------------------------- 1 | class JWTDecoder { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const decodeBtn = document.getElementById('decode-jwt'); 8 | const jwtInput = document.getElementById('jwt-input'); 9 | 10 | if (decodeBtn) { 11 | decodeBtn.addEventListener('click', () => this.decodeJWT()); 12 | } 13 | 14 | if (jwtInput) { 15 | jwtInput.addEventListener('input', () => this.clearOutput()); 16 | } 17 | } 18 | 19 | decodeJWT() { 20 | const input = document.getElementById('jwt-input').value.trim(); 21 | const headerOutput = document.getElementById('jwt-header'); 22 | const payloadOutput = document.getElementById('jwt-payload'); 23 | const signatureOutput = document.getElementById('jwt-signature'); 24 | 25 | if (!input) { 26 | window.app?.showMessage('Please enter a JWT token to decode.', 'error'); 27 | return; 28 | } 29 | 30 | try { 31 | const parts = input.split('.'); 32 | 33 | if (parts.length !== 3) { 34 | throw new Error('Invalid JWT format. JWT should have 3 parts separated by dots.'); 35 | } 36 | 37 | const [headerPart, payloadPart, signaturePart] = parts; 38 | 39 | const header = this.decodeJWTPart(headerPart); 40 | const payload = this.decodeJWTPart(payloadPart); 41 | 42 | headerOutput.textContent = JSON.stringify(header, null, 2); 43 | payloadOutput.textContent = JSON.stringify(payload, null, 2); 44 | signatureOutput.textContent = signaturePart; 45 | 46 | this.highlightClaimsInfo(payload); 47 | 48 | window.app?.showMessage('JWT decoded successfully!', 'success'); 49 | } catch (error) { 50 | window.app?.showMessage('Error decoding JWT: ' + error.message, 'error'); 51 | this.clearOutput(); 52 | } 53 | } 54 | 55 | decodeJWTPart(part) { 56 | try { 57 | const base64 = part.replace(/-/g, '+').replace(/_/g, '/'); 58 | const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '='); 59 | const decoded = atob(padded); 60 | return JSON.parse(decoded); 61 | } catch (error) { 62 | throw new Error('Failed to decode JWT part: ' + error.message); 63 | } 64 | } 65 | 66 | highlightClaimsInfo(payload) { 67 | const payloadOutput = document.getElementById('jwt-payload'); 68 | let infoHtml = JSON.stringify(payload, null, 2); 69 | 70 | if (payload.exp) { 71 | const expDate = new Date(payload.exp * 1000); 72 | const now = new Date(); 73 | const isExpired = expDate < now; 74 | 75 | infoHtml += `\n\n/* Token Information */\n`; 76 | infoHtml += `/* Expires: ${expDate.toLocaleString()} */\n`; 77 | infoHtml += `/* Status: ${isExpired ? '🔴 EXPIRED' : '🟢 VALID'} */\n`; 78 | 79 | if (payload.iat) { 80 | const iatDate = new Date(payload.iat * 1000); 81 | infoHtml += `/* Issued: ${iatDate.toLocaleString()} */\n`; 82 | } 83 | 84 | if (payload.nbf) { 85 | const nbfDate = new Date(payload.nbf * 1000); 86 | infoHtml += `/* Not Before: ${nbfDate.toLocaleString()} */\n`; 87 | } 88 | } 89 | 90 | if (payload.iss) { 91 | infoHtml += `/* Issuer: ${payload.iss} */\n`; 92 | } 93 | 94 | if (payload.aud) { 95 | infoHtml += `/* Audience: ${Array.isArray(payload.aud) ? payload.aud.join(', ') : payload.aud} */\n`; 96 | } 97 | 98 | if (payload.sub) { 99 | infoHtml += `/* Subject: ${payload.sub} */\n`; 100 | } 101 | 102 | payloadOutput.textContent = infoHtml; 103 | } 104 | 105 | clearOutput() { 106 | document.getElementById('jwt-header').textContent = ''; 107 | document.getElementById('jwt-payload').textContent = ''; 108 | document.getElementById('jwt-signature').textContent = ''; 109 | } 110 | 111 | copySection(section) { 112 | const element = document.getElementById(`jwt-${section}`); 113 | if (element && element.textContent.trim()) { 114 | navigator.clipboard.writeText(element.textContent).then(() => { 115 | window.app?.showMessage(`JWT ${section} copied to clipboard!`, 'success'); 116 | }); 117 | } 118 | } 119 | } 120 | 121 | window.JWTDecoder = new JWTDecoder(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DevTools Desktop 2 | 3 | Offline-first developer utilities for macOS, Windows, and Linux. No cloud dependencies, no data collection. 4 | 5 | ![DevTools Desktop](https://github.com/me-shaon/devtools/blob/main/assets/screenshot.png?raw=true) 6 | 7 | ## Features 8 | 9 | ### Text & Code 10 | 11 | - **JSON Viewer** - Format, validate, and minify JSON with syntax highlighting 12 | - **API Response Formatter** - Format REST/GraphQL responses with interactive tree view and real-time search 13 | - **Text Compare** - Diff viewer with line-by-line comparison 14 | - **Case Converter** - Transform between camelCase, snake_case, kebab-case, and more 15 | - **SQL Formatter** - Format SQL queries with customizable indentation 16 | - **Markdown Editor** - Live preview with export support 17 | - **Code Playground** - Multi-language editor (JS, HTML, CSS, JSON, Markdown) 18 | 19 | ### Converters 20 | 21 | - **Base64** - Encode/decode text and files 22 | - **URL Encoder** - Encode/decode URLs and URI components 23 | - **CSV ↔ JSON** - Bidirectional conversion with custom delimiters 24 | - **JSON → TypeScript** - Generate TypeScript interfaces from JSON 25 | - **Number Base** - Convert between decimal, binary, octal, hex 26 | - **Unit Converter** - Convert length, weight, temperature & currency units 27 | 28 | ### Generators 29 | 30 | - **UUID** - Generate v1/v4 UUIDs with batch support 31 | - **Password Generator** - Generate secure passwords and passphrases with strength analysis 32 | - **Hash** - MD5, SHA-1, SHA-256, SHA-512 33 | - **QR Code** - Generate QR codes with custom size and error correction 34 | - **Color Palette** - Create monochromatic, analogous, complementary schemes 35 | - **Lorem Ipsum** - Generate placeholder text 36 | 37 | ### Developer Utilities 38 | 39 | - **JWT Decoder** - Decode and inspect JWT tokens 40 | - **Regex Builder** - Interactive regex construction with live testing 41 | - **Cron Parser** - Build cron expressions with plain English descriptions 42 | - **Timestamp Converter** - Unix timestamp conversion with multiple formats 43 | - **Image Converter** - Convert between PNG, JPEG, WebP, BMP 44 | 45 | ## Quick Start 46 | 47 | ```bash 48 | # Clone and install 49 | git clone https://github.com/me-shaon/devtools.git 50 | cd devtools 51 | npm install 52 | 53 | # Run locally 54 | npm start 55 | 56 | # Build for distribution 57 | npm run dist 58 | ``` 59 | 60 | ## Requirements 61 | 62 | - Node.js 14+ 63 | - npm or yarn 64 | 65 | ## Architecture 66 | 67 | ``` 68 | src/ 69 | ├── main.js # Electron main process 70 | └── renderer/ 71 | ├── index.html # Single-page application 72 | ├── styles/ # CSS modules 73 | └── js/ 74 | ├── main.js # App router and state 75 | └── tools/ # Tool implementations (23 modules) 76 | ``` 77 | 78 | Each tool is a self-contained module with no external dependencies. All processing happens client-side. 79 | 80 | 81 | ## Development 82 | 83 | ### Adding a Tool 84 | 85 | 1. Create module in `src/renderer/js/tools/` 86 | 2. Add HTML section in `index.html` 87 | 3. Register in navigation sidebar 88 | 4. Add menu item in `src/main.js` 89 | 90 | ### Build Scripts 91 | 92 | ```bash 93 | npm start # Development server 94 | npm run build # Build executable 95 | npm run pack # Package without distributing 96 | npm run dist # Create installer 97 | npm test # Run test suite 98 | npm run security-check # Run security checks 99 | ``` 100 | 101 | ## Security 102 | 103 | This project uses GitGuardian for secret detection and follows security best practices: 104 | 105 | - 🔒 **Secret Scanning**: Automated detection of hardcoded secrets 106 | - 🛡️ **Pre-commit Hooks**: Prevent secrets from being committed 107 | - 📋 **Security Guidelines**: See [SECURITY.md](SECURITY.md) for details 108 | - 🔧 **Local Testing**: Run `npm run security-check` before committing 109 | 110 | For security issues, please review our [Security Guidelines](SECURITY.md). 111 | 112 | ## Tech Stack 113 | 114 | - Electron 22+ for desktop runtime 115 | - Vanilla JavaScript (ES6+) 116 | - No framework dependencies 117 | - No analytics or telemetry 118 | 119 | ## Contributing 120 | 121 | We welcome contributions! This project is continuously being improved by user feedback and contributions. 122 | 123 | ### Pull Requests 124 | 125 | Pull requests welcome. For major changes, open an issue first to discuss the proposed changes. 126 | 127 | ### Development Setup 128 | 129 | ```bash 130 | git clone https://github.com/me-shaon/devtools.git 131 | cd devtools-desktop 132 | npm install 133 | npm start # Run in development mode 134 | npm test # Run test suite 135 | ``` 136 | 137 | ## License 138 | 139 | MIT 140 | -------------------------------------------------------------------------------- /src/utils/lorem-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lorem utilities for generating lorem ipsum text 3 | */ 4 | 5 | class LoremUtils { 6 | static LOREM_WORDS = [ 7 | 'lorem', 8 | 'ipsum', 9 | 'dolor', 10 | 'sit', 11 | 'amet', 12 | 'consectetur', 13 | 'adipiscing', 14 | 'elit', 15 | 'sed', 16 | 'do', 17 | 'eiusmod', 18 | 'tempor', 19 | 'incididunt', 20 | 'ut', 21 | 'labore', 22 | 'et', 23 | 'dolore', 24 | 'magna', 25 | 'aliqua', 26 | 'enim', 27 | 'ad', 28 | 'minim', 29 | 'veniam', 30 | 'quis', 31 | 'nostrud', 32 | 'exercitation', 33 | 'ullamco', 34 | 'laboris', 35 | 'nisi', 36 | 'aliquip', 37 | 'ex', 38 | 'ea', 39 | 'commodo', 40 | 'consequat', 41 | 'duis', 42 | 'aute', 43 | 'irure', 44 | 'in', 45 | 'reprehenderit', 46 | 'voluptate', 47 | 'velit', 48 | 'esse', 49 | 'cillum', 50 | 'fugiat', 51 | 'nulla', 52 | 'pariatur', 53 | 'excepteur', 54 | 'sint', 55 | 'occaecat', 56 | 'cupidatat', 57 | 'non', 58 | 'proident', 59 | 'sunt', 60 | 'culpa', 61 | 'qui', 62 | 'officia', 63 | 'deserunt', 64 | 'mollit', 65 | 'anim', 66 | 'id', 67 | 'est', 68 | 'laborum', 69 | 'at', 70 | 'vero', 71 | 'eos', 72 | 'accusamus', 73 | 'accusantium', 74 | 'doloremque', 75 | 'laudantium', 76 | 'totam', 77 | 'rem', 78 | 'aperiam', 79 | 'eaque', 80 | 'ipsa', 81 | 'quae', 82 | 'ab', 83 | 'illo', 84 | 'inventore', 85 | 'veritatis', 86 | 'et', 87 | 'quasi', 88 | 'architecto', 89 | 'beatae', 90 | 'vitae', 91 | 'dicta', 92 | 'sunt', 93 | 'explicabo', 94 | 'nemo', 95 | 'ipsam', 96 | 'voluptatem', 97 | 'quia', 98 | 'voluptas', 99 | 'aspernatur', 100 | 'aut', 101 | 'odit', 102 | 'fugit', 103 | 'sed', 104 | 'quia', 105 | 'consequuntur', 106 | 'magni', 107 | 'dolores', 108 | 'ratione', 109 | 'sequi', 110 | 'nesciunt', 111 | 'neque', 112 | 'porro', 113 | 'quisquam', 114 | 'dolorem', 115 | 'adipisci', 116 | 'numquam', 117 | 'eius', 118 | 'modi', 119 | 'tempora', 120 | 'incidunt', 121 | 'magnam', 122 | 'quaerat', 123 | 'voluptatem', 124 | 'laudantium', 125 | 'doloremque', 126 | 'laudantium', 127 | ]; 128 | 129 | static generateWords(count) { 130 | if (count < 1) { 131 | throw new Error('Word count must be at least 1'); 132 | } 133 | 134 | const words = []; 135 | for (let i = 0; i < count; i++) { 136 | const word = 137 | this.LOREM_WORDS[Math.floor(Math.random() * this.LOREM_WORDS.length)]; 138 | words.push(word); 139 | } 140 | return words.join(' '); 141 | } 142 | 143 | static generateSentences(count) { 144 | if (count < 1) { 145 | throw new Error('Sentence count must be at least 1'); 146 | } 147 | 148 | const sentences = []; 149 | for (let i = 0; i < count; i++) { 150 | const wordCount = this.randomBetween(8, 20); 151 | const words = this.generateWords(wordCount).split(' '); 152 | 153 | words[0] = this.capitalizeFirst(words[0]); 154 | 155 | let sentence = words.join(' '); 156 | 157 | if (Math.random() < 0.1) { 158 | sentence += '!'; 159 | } else if (Math.random() < 0.1) { 160 | sentence += '?'; 161 | } else { 162 | sentence += '.'; 163 | } 164 | 165 | sentences.push(sentence); 166 | } 167 | return sentences.join(' '); 168 | } 169 | 170 | static generateParagraphs(count) { 171 | if (count < 1) { 172 | throw new Error('Paragraph count must be at least 1'); 173 | } 174 | 175 | const paragraphs = []; 176 | for (let i = 0; i < count; i++) { 177 | const sentenceCount = this.randomBetween(3, 7); 178 | const sentences = this.generateSentences(sentenceCount); 179 | paragraphs.push(sentences); 180 | } 181 | return paragraphs.join('\n\n'); 182 | } 183 | 184 | static generateLorem(type = 'paragraphs', count = 1) { 185 | if (count < 1 || count > 50) { 186 | throw new Error('Count must be between 1 and 50'); 187 | } 188 | 189 | switch (type) { 190 | case 'paragraphs': 191 | return this.generateParagraphs(count); 192 | case 'sentences': 193 | return this.generateSentences(count); 194 | case 'words': 195 | return this.generateWords(count); 196 | default: 197 | return this.generateParagraphs(count); 198 | } 199 | } 200 | 201 | static randomBetween(min, max) { 202 | return Math.floor(Math.random() * (max - min + 1)) + min; 203 | } 204 | 205 | static capitalizeFirst(str) { 206 | if (!str) return str; 207 | return str.charAt(0).toUpperCase() + str.slice(1); 208 | } 209 | } 210 | 211 | module.exports = LoremUtils; 212 | -------------------------------------------------------------------------------- /tests/tools/lorem-generator.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for Lorem Generator utilities 3 | */ 4 | const LoremUtils = require('../../src/utils/lorem-utils'); 5 | 6 | describe('LoremUtils', () => { 7 | describe('generateWords', () => { 8 | test('generates requested number of words', () => { 9 | const words = LoremUtils.generateWords(10); 10 | expect(words.split(' ')).toHaveLength(10); 11 | }); 12 | 13 | test('generates words from lorem dictionary', () => { 14 | const words = LoremUtils.generateWords(5); 15 | const wordArray = words.split(' '); 16 | 17 | wordArray.forEach((word) => { 18 | expect(LoremUtils.LOREM_WORDS).toContain(word); 19 | }); 20 | }); 21 | 22 | test('throws error for invalid count', () => { 23 | expect(() => LoremUtils.generateWords(0)).toThrow( 24 | 'Word count must be at least 1' 25 | ); 26 | expect(() => LoremUtils.generateWords(-1)).toThrow( 27 | 'Word count must be at least 1' 28 | ); 29 | }); 30 | 31 | test('returns string for single word', () => { 32 | const word = LoremUtils.generateWords(1); 33 | expect(typeof word).toBe('string'); 34 | expect(word.split(' ')).toHaveLength(1); 35 | }); 36 | 37 | test('returns different results on subsequent calls', () => { 38 | const words1 = LoremUtils.generateWords(20); 39 | const words2 = LoremUtils.generateWords(20); 40 | expect(words1).not.toBe(words2); 41 | }); 42 | }); 43 | 44 | describe('generateSentences', () => { 45 | test('generates requested number of sentences', () => { 46 | const sentences = LoremUtils.generateSentences(3); 47 | const sentenceArray = sentences.match(/[.!?]/g); 48 | expect(sentenceArray).toHaveLength(3); 49 | }); 50 | 51 | test('sentences are capitalized and end with punctuation', () => { 52 | const sentences = LoremUtils.generateSentences(5); 53 | const sentenceArray = sentences.match(/[.!?]/g); 54 | expect(sentenceArray).toHaveLength(5); 55 | 56 | const firstWords = sentences.match(/^[A-Z]|[.!?]\s[A-Z]/g); 57 | expect(firstWords.length).toBeGreaterThan(0); 58 | }); 59 | 60 | test('throws error for invalid count', () => { 61 | expect(() => LoremUtils.generateSentences(0)).toThrow( 62 | 'Sentence count must be at least 1' 63 | ); 64 | expect(() => LoremUtils.generateSentences(-1)).toThrow( 65 | 'Sentence count must be at least 1' 66 | ); 67 | }); 68 | }); 69 | 70 | describe('generateParagraphs', () => { 71 | test('generates requested number of paragraphs', () => { 72 | const paragraphs = LoremUtils.generateParagraphs(2); 73 | const paragraphArray = paragraphs.split('\n\n'); 74 | expect(paragraphArray).toHaveLength(2); 75 | }); 76 | 77 | test('throws error for invalid count', () => { 78 | expect(() => LoremUtils.generateParagraphs(0)).toThrow( 79 | 'Paragraph count must be at least 1' 80 | ); 81 | expect(() => LoremUtils.generateParagraphs(-1)).toThrow( 82 | 'Paragraph count must be at least 1' 83 | ); 84 | }); 85 | }); 86 | 87 | describe('generateLorem', () => { 88 | test('generates paragraphs by default', () => { 89 | const lorem = LoremUtils.generateLorem(); 90 | expect(typeof lorem).toBe('string'); 91 | expect(lorem.length).toBeGreaterThan(0); 92 | expect(lorem.split('\n\n')).toHaveLength(1); 93 | }); 94 | 95 | test('generates words when type is "words"', () => { 96 | const lorem = LoremUtils.generateLorem('words', 5); 97 | expect(lorem.split(' ')).toHaveLength(5); 98 | }); 99 | 100 | test('generates sentences when type is "sentences"', () => { 101 | const lorem = LoremUtils.generateLorem('sentences', 3); 102 | const sentenceCount = lorem.match(/[.!?]/g).length; 103 | expect(sentenceCount).toBe(3); 104 | }); 105 | 106 | test('generates paragraphs when type is "paragraphs"', () => { 107 | const lorem = LoremUtils.generateLorem('paragraphs', 2); 108 | expect(lorem.split('\n\n')).toHaveLength(2); 109 | }); 110 | 111 | test('throws error for invalid count range', () => { 112 | expect(() => LoremUtils.generateLorem('words', 0)).toThrow( 113 | 'Count must be between 1 and 50' 114 | ); 115 | expect(() => LoremUtils.generateLorem('words', 51)).toThrow( 116 | 'Count must be between 1 and 50' 117 | ); 118 | }); 119 | }); 120 | 121 | describe('utility methods', () => { 122 | test('randomBetween generates number in range', () => { 123 | for (let i = 0; i < 10; i++) { 124 | const num = LoremUtils.randomBetween(5, 10); 125 | expect(num).toBeGreaterThanOrEqual(5); 126 | expect(num).toBeLessThanOrEqual(10); 127 | } 128 | }); 129 | 130 | test('capitalizeFirst capitalizes first letter', () => { 131 | expect(LoremUtils.capitalizeFirst('hello')).toBe('Hello'); 132 | expect(LoremUtils.capitalizeFirst('WORLD')).toBe('WORLD'); 133 | expect(LoremUtils.capitalizeFirst('')).toBe(''); 134 | }); 135 | 136 | test('capitalizeFirst handles edge cases', () => { 137 | expect(LoremUtils.capitalizeFirst(null)).toBe(null); 138 | expect(LoremUtils.capitalizeFirst(undefined)).toBe(undefined); 139 | expect(LoremUtils.capitalizeFirst('a')).toBe('A'); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/renderer/js/tools/json-viewer.js: -------------------------------------------------------------------------------- 1 | class JSONViewer { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const formatBtn = document.getElementById('format-json'); 8 | const minifyBtn = document.getElementById('minify-json'); 9 | const validateBtn = document.getElementById('validate-json'); 10 | const inputTextarea = document.getElementById('json-input'); 11 | const outputPre = document.getElementById('json-output'); 12 | 13 | if (formatBtn) { 14 | formatBtn.addEventListener('click', () => this.formatJSON()); 15 | } 16 | 17 | if (minifyBtn) { 18 | minifyBtn.addEventListener('click', () => this.minifyJSON()); 19 | } 20 | 21 | if (validateBtn) { 22 | validateBtn.addEventListener('click', () => this.validateJSON()); 23 | } 24 | 25 | if (inputTextarea) { 26 | inputTextarea.addEventListener('input', () => this.clearOutput()); 27 | } 28 | } 29 | 30 | formatJSON() { 31 | const input = document.getElementById('json-input').value; 32 | const output = document.getElementById('json-output'); 33 | 34 | if (!input.trim()) { 35 | this.showError('Please enter JSON data to format.'); 36 | return; 37 | } 38 | 39 | try { 40 | const parsed = JSON.parse(input); 41 | const formatted = JSON.stringify(parsed, null, 2); 42 | output.textContent = formatted; 43 | output.className = 'formatted-json'; 44 | this.clearErrors(); 45 | window.app?.showMessage('JSON formatted successfully!', 'success'); 46 | } catch (error) { 47 | this.showError(`Invalid JSON: ${error.message}`); 48 | } 49 | } 50 | 51 | minifyJSON() { 52 | const input = document.getElementById('json-input').value; 53 | const output = document.getElementById('json-output'); 54 | 55 | if (!input.trim()) { 56 | this.showError('Please enter JSON data to minify.'); 57 | return; 58 | } 59 | 60 | try { 61 | const parsed = JSON.parse(input); 62 | const minified = JSON.stringify(parsed); 63 | output.textContent = minified; 64 | output.className = 'minified-json'; 65 | this.clearErrors(); 66 | window.app?.showMessage('JSON minified successfully!', 'success'); 67 | } catch (error) { 68 | this.showError(`Invalid JSON: ${error.message}`); 69 | } 70 | } 71 | 72 | validateJSON() { 73 | const input = document.getElementById('json-input').value; 74 | const output = document.getElementById('json-output'); 75 | 76 | if (!input.trim()) { 77 | this.showError('Please enter JSON data to validate.'); 78 | return; 79 | } 80 | 81 | try { 82 | const parsed = JSON.parse(input); 83 | const info = this.getJSONInfo(parsed); 84 | output.innerHTML = `

✅ Valid JSON

Type: ${info.type}

Size: ${info.size} characters

Keys: ${info.keys}

Depth: ${info.depth}

`; 85 | this.clearErrors(); 86 | window.app?.showMessage('JSON is valid!', 'success'); 87 | } catch (error) { 88 | this.showError(`Invalid JSON: ${error.message}`); 89 | output.innerHTML = `

❌ Invalid JSON

${error.message}

`; 90 | } 91 | } 92 | 93 | getJSONInfo(obj) { 94 | const type = Array.isArray(obj) ? 'Array' : typeof obj; 95 | const size = JSON.stringify(obj).length; 96 | const keys = this.countKeys(obj); 97 | const depth = this.getMaxDepth(obj); 98 | 99 | return { type, size, keys, depth }; 100 | } 101 | 102 | countKeys(obj) { 103 | if (typeof obj !== 'object' || obj === null) return 0; 104 | if (Array.isArray(obj)) return obj.length; 105 | 106 | let count = 0; 107 | for (let key in obj) { 108 | if (obj.hasOwnProperty(key)) { 109 | count++; 110 | if (typeof obj[key] === 'object' && obj[key] !== null) { 111 | count += this.countKeys(obj[key]); 112 | } 113 | } 114 | } 115 | return count; 116 | } 117 | 118 | getMaxDepth(obj, depth = 0) { 119 | if (typeof obj !== 'object' || obj === null) return depth; 120 | 121 | let maxDepth = depth; 122 | for (let key in obj) { 123 | if (obj.hasOwnProperty(key)) { 124 | maxDepth = Math.max(maxDepth, this.getMaxDepth(obj[key], depth + 1)); 125 | } 126 | } 127 | return maxDepth; 128 | } 129 | 130 | showError(message) { 131 | const output = document.getElementById('json-output'); 132 | output.innerHTML = `
${message}
`; 133 | } 134 | 135 | clearErrors() { 136 | const existingErrors = document.querySelectorAll('.error'); 137 | existingErrors.forEach(error => error.remove()); 138 | } 139 | 140 | clearOutput() { 141 | const output = document.getElementById('json-output'); 142 | output.textContent = ''; 143 | } 144 | } 145 | 146 | window.JSONViewer = new JSONViewer(); -------------------------------------------------------------------------------- /src/utils/password-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Password utilities for generating secure passwords and passphrases 3 | */ 4 | 5 | class PasswordUtils { 6 | static LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'; 7 | static UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 8 | static NUMBERS = '0123456789'; 9 | static SYMBOLS = '!@#$%^&*()_+-=[]{};\':"|,.<>?'; 10 | static AMBIGUOUS = '0OIl1'; 11 | 12 | // Word list for passphrases (subset for testing) 13 | static WORDS = [ 14 | 'apple', 15 | 'brave', 16 | 'chair', 17 | 'dream', 18 | 'eagle', 19 | 'flame', 20 | 'grape', 21 | 'house', 22 | 'image', 23 | 'judge', 24 | 'knife', 25 | 'lemon', 26 | 'magic', 27 | 'novel', 28 | 'ocean', 29 | 'piano', 30 | 'queen', 31 | 'river', 32 | 'stone', 33 | 'table', 34 | 'unity', 35 | 'value', 36 | 'water', 37 | 'youth', 38 | 'zebra', 39 | 'beach', 40 | 'cloud', 41 | 'dance', 42 | 'earth', 43 | 'field', 44 | 'green', 45 | 'happy', 46 | 'light', 47 | 'mouse', 48 | 'night', 49 | 'party', 50 | 'quick', 51 | 'smile', 52 | 'trust', 53 | 'voice', 54 | ]; 55 | 56 | static generatePassword(options = {}) { 57 | const { 58 | length = 12, 59 | includeLowercase = true, 60 | includeUppercase = true, 61 | includeNumbers = true, 62 | includeSymbols = false, 63 | excludeAmbiguous = false, 64 | } = options; 65 | 66 | let charset = ''; 67 | 68 | // Build character set based on options 69 | if (includeLowercase) charset += this.LOWERCASE; 70 | if (includeUppercase) charset += this.UPPERCASE; 71 | if (includeNumbers) charset += this.NUMBERS; 72 | if (includeSymbols) charset += this.SYMBOLS; 73 | 74 | if (!charset) { 75 | throw new Error('At least one character set must be selected'); 76 | } 77 | 78 | // Remove ambiguous characters if requested 79 | if (excludeAmbiguous) { 80 | for (const char of this.AMBIGUOUS) { 81 | charset = charset.replace(new RegExp(char, 'g'), ''); 82 | } 83 | } 84 | 85 | // Generate password 86 | let password = ''; 87 | for (let i = 0; i < length; i++) { 88 | const randomIndex = Math.floor(Math.random() * charset.length); 89 | password += charset[randomIndex]; 90 | } 91 | 92 | return password; 93 | } 94 | 95 | static generateMultiplePasswords(count, options = {}) { 96 | const passwords = []; 97 | for (let i = 0; i < count; i++) { 98 | passwords.push(this.generatePassword(options)); 99 | } 100 | return passwords; 101 | } 102 | 103 | static calculatePasswordStrength(password) { 104 | if (!password) { 105 | return { 106 | score: 0, 107 | strength: 'Very Weak', 108 | feedback: ['Password is required'], 109 | entropy: 0, 110 | }; 111 | } 112 | 113 | let score = 0; 114 | let feedback = []; 115 | 116 | // Length check 117 | if (password.length >= 12) score += 2; 118 | else if (password.length >= 8) score += 1; 119 | else feedback.push('Use at least 8 characters'); 120 | 121 | // Character variety checks 122 | if (/[a-z]/.test(password)) score += 1; 123 | else feedback.push('Add lowercase letters'); 124 | 125 | if (/[A-Z]/.test(password)) score += 1; 126 | else feedback.push('Add uppercase letters'); 127 | 128 | if (/[0-9]/.test(password)) score += 1; 129 | else feedback.push('Add numbers'); 130 | 131 | if (/[^a-zA-Z0-9]/.test(password)) score += 1; 132 | else feedback.push('Add symbols'); 133 | 134 | // Common pattern penalties 135 | if (/(.)\1{2,}/.test(password)) { 136 | score -= 1; 137 | feedback.push('Avoid repeated characters'); 138 | } 139 | 140 | if ( 141 | /123|234|345|456|567|678|789|abc|bcd|cde|def/.test(password.toLowerCase()) 142 | ) { 143 | score -= 1; 144 | feedback.push('Avoid sequential characters'); 145 | } 146 | 147 | // Calculate entropy 148 | let charset = 0; 149 | if (/[a-z]/.test(password)) charset += 26; 150 | if (/[A-Z]/.test(password)) charset += 26; 151 | if (/[0-9]/.test(password)) charset += 10; 152 | if (/[^a-zA-Z0-9]/.test(password)) charset += 32; 153 | 154 | const entropy = Math.log2(Math.pow(charset, password.length)); 155 | 156 | // Determine strength label 157 | let strength; 158 | if (score < 1) strength = 'Very Weak'; 159 | else if (score < 2) strength = 'Weak'; 160 | else if (score < 4) strength = 'Fair'; 161 | else if (score < 6) strength = 'Good'; 162 | else strength = 'Strong'; 163 | 164 | return { 165 | score: Math.max(0, score), 166 | strength, 167 | feedback, 168 | entropy: Math.round(entropy), 169 | }; 170 | } 171 | 172 | static generatePassphrase(options = {}) { 173 | const { 174 | wordCount = 4, 175 | separator = '-', 176 | capitalize = false, 177 | includeNumbers = false, 178 | } = options; 179 | 180 | const selectedWords = []; 181 | 182 | // Select random words 183 | for (let i = 0; i < wordCount; i++) { 184 | const randomIndex = Math.floor(Math.random() * this.WORDS.length); 185 | let word = this.WORDS[randomIndex]; 186 | 187 | if (capitalize) { 188 | word = word.charAt(0).toUpperCase() + word.slice(1); 189 | } 190 | 191 | selectedWords.push(word); 192 | } 193 | 194 | let passphrase = selectedWords.join(separator); 195 | 196 | // Add numbers if requested 197 | if (includeNumbers) { 198 | const randomNum = Math.floor(Math.random() * 1000); 199 | passphrase += randomNum; 200 | } 201 | 202 | return passphrase; 203 | } 204 | } 205 | 206 | module.exports = PasswordUtils; 207 | -------------------------------------------------------------------------------- /src/renderer/js/tools/json-to-ts.js: -------------------------------------------------------------------------------- 1 | class JSONToTypeScript { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const convertBtn = document.getElementById('convert-to-ts'); 8 | const clearBtn = document.getElementById('clear-ts'); 9 | const inputTextarea = document.getElementById('json-to-ts-input'); 10 | 11 | if (convertBtn) { 12 | convertBtn.addEventListener('click', () => this.convertToTypeScript()); 13 | } 14 | 15 | if (clearBtn) { 16 | clearBtn.addEventListener('click', () => this.clearAll()); 17 | } 18 | 19 | if (inputTextarea) { 20 | inputTextarea.addEventListener('input', () => this.clearOutput()); 21 | } 22 | } 23 | 24 | convertToTypeScript() { 25 | const input = document.getElementById('json-to-ts-input').value.trim(); 26 | const output = document.getElementById('typescript-output'); 27 | 28 | if (!input) { 29 | window.app?.showMessage('Please enter JSON data to convert.', 'error'); 30 | return; 31 | } 32 | 33 | try { 34 | const parsed = JSON.parse(input); 35 | const typescript = this.generateTypeScript(parsed, 'RootObject'); 36 | output.textContent = typescript; 37 | window.app?.showMessage('JSON converted to TypeScript!', 'success'); 38 | } catch (error) { 39 | window.app?.showMessage('Error: Invalid JSON format', 'error'); 40 | output.textContent = ''; 41 | } 42 | } 43 | 44 | generateTypeScript(obj, interfaceName = 'RootObject', level = 0) { 45 | const indent = ' '.repeat(level); 46 | let result = ''; 47 | 48 | if (Array.isArray(obj)) { 49 | if (obj.length === 0) { 50 | return 'any[]'; 51 | } 52 | const firstItem = obj[0]; 53 | if (typeof firstItem === 'object' && firstItem !== null) { 54 | const itemInterface = this.generateTypeScript(firstItem, interfaceName + 'Item', level + 1); 55 | return itemInterface + '\n' + indent + `type ${interfaceName} = ${interfaceName}Item[];`; 56 | } else { 57 | return `${this.getTypeScriptType(firstItem)}[]`; 58 | } 59 | } 60 | 61 | if (typeof obj === 'object' && obj !== null) { 62 | result += `${indent}interface ${interfaceName} {\n`; 63 | 64 | const nestedInterfaces = []; 65 | 66 | for (const [key, value] of Object.entries(obj)) { 67 | const safeKey = this.makeSafePropertyName(key); 68 | const optional = this.shouldBeOptional(value) ? '?' : ''; 69 | 70 | if (Array.isArray(value)) { 71 | if (value.length === 0) { 72 | result += `${indent} ${safeKey}${optional}: any[];\n`; 73 | } else { 74 | const firstItem = value[0]; 75 | if (typeof firstItem === 'object' && firstItem !== null) { 76 | const itemInterfaceName = this.capitalizeFirst(safeKey) + 'Item'; 77 | const itemInterface = this.generateTypeScript(firstItem, itemInterfaceName, level + 1); 78 | nestedInterfaces.push(itemInterface); 79 | result += `${indent} ${safeKey}${optional}: ${itemInterfaceName}[];\n`; 80 | } else { 81 | result += `${indent} ${safeKey}${optional}: ${this.getTypeScriptType(firstItem)}[];\n`; 82 | } 83 | } 84 | } else if (typeof value === 'object' && value !== null) { 85 | const nestedInterfaceName = this.capitalizeFirst(safeKey); 86 | const nestedInterface = this.generateTypeScript(value, nestedInterfaceName, level + 1); 87 | nestedInterfaces.push(nestedInterface); 88 | result += `${indent} ${safeKey}${optional}: ${nestedInterfaceName};\n`; 89 | } else { 90 | result += `${indent} ${safeKey}${optional}: ${this.getTypeScriptType(value)};\n`; 91 | } 92 | } 93 | 94 | result += `${indent}}\n`; 95 | 96 | if (nestedInterfaces.length > 0) { 97 | result = nestedInterfaces.join('\n') + '\n' + result; 98 | } 99 | 100 | return result; 101 | } 102 | 103 | return this.getTypeScriptType(obj); 104 | } 105 | 106 | getTypeScriptType(value) { 107 | if (value === null) return 'any'; 108 | if (typeof value === 'string') return 'string'; 109 | if (typeof value === 'number') return 'number'; 110 | if (typeof value === 'boolean') return 'boolean'; 111 | if (Array.isArray(value)) return 'any[]'; 112 | if (typeof value === 'object') return 'object'; 113 | return 'any'; 114 | } 115 | 116 | makeSafePropertyName(key) { 117 | if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) { 118 | return key; 119 | } 120 | return `"${key}"`; 121 | } 122 | 123 | shouldBeOptional(value) { 124 | return value === null || value === undefined; 125 | } 126 | 127 | capitalizeFirst(str) { 128 | return str.charAt(0).toUpperCase() + str.slice(1); 129 | } 130 | 131 | clearAll() { 132 | document.getElementById('json-to-ts-input').value = ''; 133 | this.clearOutput(); 134 | window.app?.showMessage('Cleared!', 'info'); 135 | } 136 | 137 | clearOutput() { 138 | document.getElementById('typescript-output').textContent = ''; 139 | } 140 | } 141 | 142 | window.JSONToTypeScript = new JSONToTypeScript(); -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | let mainWindow; 6 | 7 | const isDev = process.argv.includes('--dev'); 8 | 9 | function createWindow() { 10 | mainWindow = new BrowserWindow({ 11 | width: 1200, 12 | height: 800, 13 | minWidth: 800, 14 | minHeight: 600, 15 | webPreferences: { 16 | nodeIntegration: true, 17 | contextIsolation: false, 18 | enableRemoteModule: true 19 | }, 20 | titleBarStyle: 'default', 21 | show: false 22 | }); 23 | 24 | mainWindow.loadFile('src/renderer/index.html'); 25 | 26 | if (isDev) { 27 | mainWindow.webContents.openDevTools(); 28 | } 29 | 30 | mainWindow.once('ready-to-show', () => { 31 | mainWindow.show(); 32 | }); 33 | 34 | mainWindow.on('closed', () => { 35 | mainWindow = null; 36 | }); 37 | 38 | createMenu(); 39 | } 40 | 41 | function createMenu() { 42 | const template = [ 43 | { 44 | label: 'File', 45 | submenu: [ 46 | { 47 | label: 'New Window', 48 | accelerator: 'CmdOrCtrl+N', 49 | click: () => { 50 | createWindow(); 51 | } 52 | }, 53 | { type: 'separator' }, 54 | { 55 | label: 'Quit', 56 | accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', 57 | click: () => { 58 | app.quit(); 59 | } 60 | } 61 | ] 62 | }, 63 | { 64 | label: 'Edit', 65 | submenu: [ 66 | { role: 'undo' }, 67 | { role: 'redo' }, 68 | { type: 'separator' }, 69 | { role: 'cut' }, 70 | { role: 'copy' }, 71 | { role: 'paste' }, 72 | { role: 'selectall' } 73 | ] 74 | }, 75 | { 76 | label: 'View', 77 | submenu: [ 78 | { role: 'reload' }, 79 | { role: 'forceReload' }, 80 | { role: 'toggleDevTools' }, 81 | { type: 'separator' }, 82 | { role: 'resetZoom' }, 83 | { role: 'zoomIn' }, 84 | { role: 'zoomOut' }, 85 | { type: 'separator' }, 86 | { role: 'togglefullscreen' } 87 | ] 88 | }, 89 | { 90 | label: 'Tools', 91 | submenu: [ 92 | { label: 'JSON Viewer', click: () => mainWindow.webContents.send('navigate-to', 'json-viewer') }, 93 | { label: 'Text Compare', click: () => mainWindow.webContents.send('navigate-to', 'text-compare') }, 94 | { label: 'Case Converter', click: () => mainWindow.webContents.send('navigate-to', 'case-converter') }, 95 | { label: 'UUID Generator', click: () => mainWindow.webContents.send('navigate-to', 'uuid-generator') }, 96 | { label: 'Base64 Converter', click: () => mainWindow.webContents.send('navigate-to', 'base64-converter') }, 97 | { label: 'JWT Decoder', click: () => mainWindow.webContents.send('navigate-to', 'jwt-decoder') }, 98 | { type: 'separator' }, 99 | { label: 'SQL Formatter', click: () => mainWindow.webContents.send('navigate-to', 'sql-formatter') }, 100 | { label: 'Markdown Editor', click: () => mainWindow.webContents.send('navigate-to', 'markdown-editor') }, 101 | { label: 'Code Playground', click: () => mainWindow.webContents.send('navigate-to', 'code-playground') }, 102 | { type: 'separator' }, 103 | { label: 'Hash Generator', click: () => mainWindow.webContents.send('navigate-to', 'hash-generator') }, 104 | { label: 'QR Code Generator', click: () => mainWindow.webContents.send('navigate-to', 'qr-generator') } 105 | ] 106 | }, 107 | { 108 | label: 'Help', 109 | submenu: [ 110 | { 111 | label: 'About', 112 | click: () => { 113 | shell.openExternal('https://github.com'); 114 | } 115 | } 116 | ] 117 | } 118 | ]; 119 | 120 | if (process.platform === 'darwin') { 121 | template.unshift({ 122 | label: app.getName(), 123 | submenu: [ 124 | { role: 'about' }, 125 | { type: 'separator' }, 126 | { role: 'services', submenu: [] }, 127 | { type: 'separator' }, 128 | { role: 'hide' }, 129 | { role: 'hideothers' }, 130 | { role: 'unhide' }, 131 | { type: 'separator' }, 132 | { role: 'quit' } 133 | ] 134 | }); 135 | } 136 | 137 | const menu = Menu.buildFromTemplate(template); 138 | Menu.setApplicationMenu(menu); 139 | } 140 | 141 | app.whenReady().then(createWindow); 142 | 143 | app.on('window-all-closed', () => { 144 | if (process.platform !== 'darwin') { 145 | app.quit(); 146 | } 147 | }); 148 | 149 | app.on('activate', () => { 150 | if (BrowserWindow.getAllWindows().length === 0) { 151 | createWindow(); 152 | } 153 | }); 154 | 155 | ipcMain.handle('save-file', async (event, data) => { 156 | const { dialog } = require('electron'); 157 | const result = await dialog.showSaveDialog(mainWindow, { 158 | filters: data.filters || [] 159 | }); 160 | 161 | if (!result.canceled && result.filePath) { 162 | try { 163 | fs.writeFileSync(result.filePath, data.content); 164 | return { success: true, path: result.filePath }; 165 | } catch (error) { 166 | return { success: false, error: error.message }; 167 | } 168 | } 169 | return { success: false, error: 'Save cancelled' }; 170 | }); 171 | 172 | ipcMain.handle('open-file', async (event, filters) => { 173 | const { dialog } = require('electron'); 174 | const result = await dialog.showOpenDialog(mainWindow, { 175 | filters: filters || [], 176 | properties: ['openFile'] 177 | }); 178 | 179 | if (!result.canceled && result.filePaths.length > 0) { 180 | try { 181 | const content = fs.readFileSync(result.filePaths[0], 'utf8'); 182 | return { success: true, content, path: result.filePaths[0] }; 183 | } catch (error) { 184 | return { success: false, error: error.message }; 185 | } 186 | } 187 | return { success: false, error: 'Open cancelled' }; 188 | }); -------------------------------------------------------------------------------- /src/renderer/js/tools/number-base.js: -------------------------------------------------------------------------------- 1 | class NumberBaseConverter { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const inputs = document.querySelectorAll( 8 | '#decimal-input, #binary-input, #octal-input, #hex-input' 9 | ); 10 | const clearBtn = document.getElementById('clear-bases'); 11 | 12 | inputs.forEach((input) => { 13 | input.addEventListener('input', (e) => this.convertFromInput(e.target)); 14 | }); 15 | 16 | if (clearBtn) { 17 | clearBtn.addEventListener('click', () => this.clearAll()); 18 | } 19 | } 20 | 21 | convertFromInput(sourceInput) { 22 | const value = sourceInput.value.trim(); 23 | if (!value) { 24 | this.clearOtherInputs(sourceInput.id); 25 | return; 26 | } 27 | 28 | try { 29 | let decimalValue; 30 | 31 | switch (sourceInput.id) { 32 | case 'decimal-input': 33 | decimalValue = this.parseDecimal(value); 34 | break; 35 | case 'binary-input': 36 | decimalValue = this.parseBinary(value); 37 | break; 38 | case 'octal-input': 39 | decimalValue = this.parseOctal(value); 40 | break; 41 | case 'hex-input': 42 | decimalValue = this.parseHex(value); 43 | break; 44 | default: 45 | return; 46 | } 47 | 48 | if (decimalValue === null) { 49 | this.showError(sourceInput.id, 'Invalid format'); 50 | return; 51 | } 52 | 53 | this.updateAllFields(decimalValue, sourceInput.id); 54 | this.clearError(sourceInput.id); 55 | } catch (error) { 56 | this.showError(sourceInput.id, error.message); 57 | } 58 | } 59 | 60 | parseDecimal(value) { 61 | const num = parseInt(value, 10); 62 | if (isNaN(num) || num < 0) return null; 63 | return num; 64 | } 65 | 66 | parseBinary(value) { 67 | if (!/^[01]+$/.test(value)) return null; 68 | return parseInt(value, 2); 69 | } 70 | 71 | parseOctal(value) { 72 | if (!/^[0-7]+$/.test(value)) return null; 73 | return parseInt(value, 8); 74 | } 75 | 76 | parseHex(value) { 77 | const cleaned = value.replace(/^0x/i, ''); 78 | if (!/^[0-9a-f]+$/i.test(cleaned)) return null; 79 | return parseInt(cleaned, 16); 80 | } 81 | 82 | updateAllFields(decimalValue, sourceInputId) { 83 | if (sourceInputId !== 'decimal-input') { 84 | document.getElementById('decimal-input').value = decimalValue.toString(); 85 | } 86 | 87 | if (sourceInputId !== 'binary-input') { 88 | document.getElementById('binary-input').value = decimalValue.toString(2); 89 | } 90 | 91 | if (sourceInputId !== 'octal-input') { 92 | document.getElementById('octal-input').value = decimalValue.toString(8); 93 | } 94 | 95 | if (sourceInputId !== 'hex-input') { 96 | document.getElementById('hex-input').value = decimalValue 97 | .toString(16) 98 | .toUpperCase(); 99 | } 100 | } 101 | 102 | clearOtherInputs(sourceInputId) { 103 | const inputs = [ 104 | 'decimal-input', 105 | 'binary-input', 106 | 'octal-input', 107 | 'hex-input', 108 | ]; 109 | inputs.forEach((inputId) => { 110 | if (inputId !== sourceInputId) { 111 | document.getElementById(inputId).value = ''; 112 | } 113 | }); 114 | } 115 | 116 | showError(inputId, message) { 117 | const input = document.getElementById(inputId); 118 | input.style.borderColor = '#dc3545'; 119 | input.title = message; 120 | 121 | let errorSpan = input.parentNode.querySelector('.error-message'); 122 | if (!errorSpan) { 123 | errorSpan = document.createElement('span'); 124 | errorSpan.className = 'error-message'; 125 | errorSpan.style.cssText = 126 | 'color: #dc3545; font-size: 12px; display: block; margin-top: 5px;'; 127 | input.parentNode.appendChild(errorSpan); 128 | } 129 | errorSpan.textContent = message; 130 | } 131 | 132 | clearError(inputId) { 133 | const input = document.getElementById(inputId); 134 | input.style.borderColor = '#f0f0f0'; 135 | input.title = ''; 136 | 137 | const errorSpan = input.parentNode.querySelector('.error-message'); 138 | if (errorSpan) { 139 | errorSpan.remove(); 140 | } 141 | } 142 | 143 | clearAll() { 144 | const inputs = [ 145 | 'decimal-input', 146 | 'binary-input', 147 | 'octal-input', 148 | 'hex-input', 149 | ]; 150 | inputs.forEach((inputId) => { 151 | document.getElementById(inputId).value = ''; 152 | this.clearError(inputId); 153 | }); 154 | window.app?.showMessage('All fields cleared!', 'info'); 155 | } 156 | 157 | copyValue(inputId) { 158 | const input = document.getElementById(inputId); 159 | if (input.value.trim()) { 160 | navigator.clipboard.writeText(input.value).then(() => { 161 | window.app?.showMessage('Value copied to clipboard!', 'success'); 162 | }); 163 | } 164 | } 165 | } 166 | 167 | window.NumberBaseConverter = new NumberBaseConverter(); 168 | 169 | const baseStyles = ` 170 | .base-inputs { 171 | display: grid; 172 | gap: 20px; 173 | margin-bottom: 20px; 174 | } 175 | 176 | .base-input-group { 177 | display: flex; 178 | flex-direction: column; 179 | gap: 8px; 180 | } 181 | 182 | .base-input-group label { 183 | font-weight: 500; 184 | color: #1d1d1f; 185 | font-size: 14px; 186 | } 187 | 188 | .base-input-group input { 189 | padding: 12px 15px; 190 | border: 2px solid #f0f0f0; 191 | border-radius: 8px; 192 | font-family: 'Monaco', 'Consolas', monospace; 193 | font-size: 14px; 194 | transition: border-color 0.3s ease; 195 | } 196 | 197 | .base-input-group input:focus { 198 | outline: none; 199 | border-color: #667eea; 200 | } 201 | 202 | .base-input-group input:invalid { 203 | border-color: #dc3545; 204 | } 205 | 206 | @media (min-width: 768px) { 207 | .base-inputs { 208 | grid-template-columns: 1fr 1fr; 209 | } 210 | } 211 | 212 | .error-message { 213 | animation: fadeIn 0.3s ease; 214 | } 215 | 216 | @keyframes fadeIn { 217 | from { opacity: 0; transform: translateY(-5px); } 218 | to { opacity: 1; transform: translateY(0); } 219 | } 220 | `; 221 | 222 | const numberBaseStyle = document.createElement('style'); 223 | numberBaseStyle.textContent = numberBaseStyles; 224 | document.head.appendChild(numberBaseStyle); 225 | -------------------------------------------------------------------------------- /src/renderer/js/tools/timestamp.js: -------------------------------------------------------------------------------- 1 | class TimestampConverter { 2 | constructor() { 3 | this.init(); 4 | this.updateCurrentTime(); 5 | this.startClock(); 6 | } 7 | 8 | init() { 9 | const tabButtons = document.querySelectorAll('#timestamp .tab-btn'); 10 | const refreshBtn = document.getElementById('refresh-time'); 11 | const convertTsBtn = document.getElementById('convert-ts-to-date'); 12 | const convertDateBtn = document.getElementById('convert-date-to-ts'); 13 | 14 | tabButtons.forEach((button) => { 15 | button.addEventListener('click', () => { 16 | this.switchTab(button.dataset.tab); 17 | }); 18 | }); 19 | 20 | if (refreshBtn) { 21 | refreshBtn.addEventListener('click', () => this.updateCurrentTime()); 22 | } 23 | 24 | if (convertTsBtn) { 25 | convertTsBtn.addEventListener('click', () => 26 | this.convertTimestampToDate() 27 | ); 28 | } 29 | 30 | if (convertDateBtn) { 31 | convertDateBtn.addEventListener('click', () => 32 | this.convertDateToTimestamp() 33 | ); 34 | } 35 | 36 | const inputs = document.querySelectorAll('#timestamp-input, #date-input'); 37 | inputs.forEach((input) => { 38 | input.addEventListener('input', () => this.clearOutputs()); 39 | }); 40 | } 41 | 42 | startClock() { 43 | this.clockInterval = setInterval(() => { 44 | this.updateCurrentTime(); 45 | }, 1000); 46 | } 47 | 48 | updateCurrentTime() { 49 | const now = new Date(); 50 | const timestamp = Math.floor(now.getTime() / 1000); 51 | const dateString = now.toLocaleString(); 52 | 53 | document.getElementById('current-timestamp').textContent = timestamp; 54 | document.getElementById('current-date').textContent = dateString; 55 | } 56 | 57 | switchTab(tabName) { 58 | const container = document.getElementById('timestamp'); 59 | 60 | container.querySelectorAll('.tab-btn').forEach((btn) => { 61 | btn.classList.remove('active'); 62 | }); 63 | container.querySelectorAll('.tab-content').forEach((content) => { 64 | content.classList.remove('active'); 65 | }); 66 | 67 | container.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); 68 | container.getElementById(`${tabName}-tab`).classList.add('active'); 69 | 70 | this.clearOutputs(); 71 | } 72 | 73 | convertTimestampToDate() { 74 | const input = document.getElementById('timestamp-input').value; 75 | const output = document.getElementById('date-output'); 76 | 77 | if (!input.trim()) { 78 | window.app?.showMessage('Please enter a timestamp.', 'error'); 79 | return; 80 | } 81 | 82 | try { 83 | let timestamp = parseInt(input); 84 | 85 | if (timestamp.toString().length === 10) { 86 | timestamp = timestamp * 1000; 87 | } else if (timestamp.toString().length !== 13) { 88 | throw new Error('Invalid timestamp format'); 89 | } 90 | 91 | const date = new Date(timestamp); 92 | 93 | if (isNaN(date.getTime())) { 94 | throw new Error('Invalid timestamp'); 95 | } 96 | 97 | const result = ` 98 | Local Time: ${date.toLocaleString()} 99 | UTC Time: ${date.toUTCString()} 100 | ISO String: ${date.toISOString()} 101 | Date Only: ${date.toDateString()} 102 | Time Only: ${date.toTimeString()} 103 | 104 | Timestamp (seconds): ${Math.floor(timestamp / 1000)} 105 | Timestamp (milliseconds): ${timestamp} 106 | `.trim(); 107 | 108 | output.value = result; 109 | window.app?.showMessage('Timestamp converted successfully!', 'success'); 110 | } catch (error) { 111 | window.app?.showMessage('Error: ' + error.message, 'error'); 112 | output.value = ''; 113 | } 114 | } 115 | 116 | convertDateToTimestamp() { 117 | const input = document.getElementById('date-input').value; 118 | const output = document.getElementById('timestamp-output'); 119 | 120 | if (!input.trim()) { 121 | window.app?.showMessage('Please select a date and time.', 'error'); 122 | return; 123 | } 124 | 125 | try { 126 | const date = new Date(input); 127 | 128 | if (isNaN(date.getTime())) { 129 | throw new Error('Invalid date'); 130 | } 131 | 132 | const timestamp = date.getTime(); 133 | const timestampSeconds = Math.floor(timestamp / 1000); 134 | 135 | const result = ` 136 | Date: ${date.toLocaleString()} 137 | UTC: ${date.toUTCString()} 138 | ISO: ${date.toISOString()} 139 | 140 | Timestamp (seconds): ${timestampSeconds} 141 | Timestamp (milliseconds): ${timestamp} 142 | `.trim(); 143 | 144 | output.value = result; 145 | window.app?.showMessage('Date converted successfully!', 'success'); 146 | } catch (error) { 147 | window.app?.showMessage('Error: ' + error.message, 'error'); 148 | output.value = ''; 149 | } 150 | } 151 | 152 | clearOutputs() { 153 | document.getElementById('date-output').value = ''; 154 | document.getElementById('timestamp-output').value = ''; 155 | } 156 | 157 | destroy() { 158 | if (this.clockInterval) { 159 | clearInterval(this.clockInterval); 160 | } 161 | } 162 | } 163 | 164 | window.TimestampConverter = new TimestampConverter(); 165 | 166 | const timestampStyles = ` 167 | .timestamp-section { 168 | background: #f8f9fa; 169 | border-radius: 8px; 170 | padding: 20px; 171 | margin-bottom: 20px; 172 | } 173 | 174 | .current-time { 175 | display: flex; 176 | align-items: center; 177 | justify-content: space-between; 178 | flex-wrap: wrap; 179 | gap: 15px; 180 | } 181 | 182 | .time-display { 183 | display: flex; 184 | flex-direction: column; 185 | gap: 5px; 186 | } 187 | 188 | .time-display strong { 189 | font-size: 1.5rem; 190 | color: #667eea; 191 | font-family: 'Monaco', 'Consolas', monospace; 192 | } 193 | 194 | .time-display span { 195 | color: #86868b; 196 | font-size: 0.9rem; 197 | } 198 | 199 | #timestamp-input, 200 | #date-input { 201 | width: 100%; 202 | padding: 10px 15px; 203 | border: 2px solid #f0f0f0; 204 | border-radius: 6px; 205 | font-size: 14px; 206 | margin-bottom: 15px; 207 | transition: border-color 0.3s ease; 208 | } 209 | 210 | #timestamp-input:focus, 211 | #date-input:focus { 212 | outline: none; 213 | border-color: #667eea; 214 | } 215 | `; 216 | 217 | const timestampStyle = document.createElement('style'); 218 | timestampStyle.textContent = timestampStyles; 219 | document.head.appendChild(timestampStyle); 220 | -------------------------------------------------------------------------------- /src/renderer/js/tools/url-encoder.js: -------------------------------------------------------------------------------- 1 | class URLEncoder { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const tabButtons = document.querySelectorAll('#url-encoder .tab-btn'); 8 | const encodeBtn = document.getElementById('url-encode-btn'); 9 | const decodeBtn = document.getElementById('url-decode-btn'); 10 | const copyEncodedBtn = document.getElementById('copy-encoded'); 11 | const copyDecodedBtn = document.getElementById('copy-decoded'); 12 | const clearEncodeBtn = document.getElementById('clear-encode'); 13 | const clearDecodeBtn = document.getElementById('clear-decode'); 14 | 15 | tabButtons.forEach(button => { 16 | button.addEventListener('click', () => { 17 | this.switchTab(button.dataset.tab); 18 | }); 19 | }); 20 | 21 | if (encodeBtn) { 22 | encodeBtn.addEventListener('click', () => this.encodeURL()); 23 | } 24 | 25 | if (decodeBtn) { 26 | decodeBtn.addEventListener('click', () => this.decodeURL()); 27 | } 28 | 29 | if (copyEncodedBtn) { 30 | copyEncodedBtn.addEventListener('click', () => this.copyToClipboard('url-encoded-output')); 31 | } 32 | 33 | if (copyDecodedBtn) { 34 | copyDecodedBtn.addEventListener('click', () => this.copyToClipboard('url-decoded-output')); 35 | } 36 | 37 | if (clearEncodeBtn) { 38 | clearEncodeBtn.addEventListener('click', () => this.clearEncode()); 39 | } 40 | 41 | if (clearDecodeBtn) { 42 | clearDecodeBtn.addEventListener('click', () => this.clearDecode()); 43 | } 44 | 45 | // Add Enter key support 46 | const encodeInput = document.getElementById('url-to-encode'); 47 | const decodeInput = document.getElementById('url-to-decode'); 48 | 49 | if (encodeInput) { 50 | encodeInput.addEventListener('keypress', (e) => { 51 | if (e.key === 'Enter' && e.ctrlKey) { 52 | this.encodeURL(); 53 | } 54 | }); 55 | } 56 | 57 | if (decodeInput) { 58 | decodeInput.addEventListener('keypress', (e) => { 59 | if (e.key === 'Enter' && e.ctrlKey) { 60 | this.decodeURL(); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | switchTab(tabName) { 67 | const encoder = document.getElementById('url-encoder'); 68 | 69 | encoder.querySelectorAll('.tab-btn').forEach(btn => { 70 | btn.classList.remove('active'); 71 | }); 72 | encoder.querySelectorAll('.tab-content').forEach(content => { 73 | content.classList.remove('active'); 74 | }); 75 | 76 | encoder.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); 77 | document.getElementById(`${tabName}-tab`).classList.add('active'); 78 | 79 | this.clearOutputs(); 80 | } 81 | 82 | encodeURL() { 83 | const input = document.getElementById('url-to-encode').value; 84 | const output = document.getElementById('url-encoded-output'); 85 | 86 | if (!input.trim()) { 87 | window.app?.showMessage('Please enter URL or text to encode.', 'error'); 88 | return; 89 | } 90 | 91 | try { 92 | // Use encodeURIComponent for full encoding 93 | const encoded = encodeURIComponent(input); 94 | output.value = encoded; 95 | 96 | // Show some stats 97 | const originalLength = input.length; 98 | const encodedLength = encoded.length; 99 | const message = `URL encoded successfully! (${originalLength} → ${encodedLength} characters)`; 100 | 101 | window.app?.showMessage(message, 'success'); 102 | this.selectOutput('url-encoded-output'); 103 | } catch (error) { 104 | window.app?.showMessage('Error encoding URL: ' + error.message, 'error'); 105 | output.value = ''; 106 | } 107 | } 108 | 109 | decodeURL() { 110 | const input = document.getElementById('url-to-decode').value; 111 | const output = document.getElementById('url-decoded-output'); 112 | 113 | if (!input.trim()) { 114 | window.app?.showMessage('Please enter encoded URL to decode.', 'error'); 115 | return; 116 | } 117 | 118 | try { 119 | const decoded = decodeURIComponent(input); 120 | output.value = decoded; 121 | 122 | // Show some stats 123 | const originalLength = input.length; 124 | const decodedLength = decoded.length; 125 | const message = `URL decoded successfully! (${originalLength} → ${decodedLength} characters)`; 126 | 127 | window.app?.showMessage(message, 'success'); 128 | this.selectOutput('url-decoded-output'); 129 | } catch (error) { 130 | window.app?.showMessage('Error decoding URL: Invalid URL encoding format', 'error'); 131 | output.value = ''; 132 | } 133 | } 134 | 135 | selectOutput(outputId) { 136 | const output = document.getElementById(outputId); 137 | setTimeout(() => { 138 | output.select(); 139 | output.setSelectionRange(0, 99999); 140 | }, 100); 141 | } 142 | 143 | copyToClipboard(outputId) { 144 | const output = document.getElementById(outputId); 145 | 146 | if (!output.value.trim()) { 147 | window.app?.showMessage('Nothing to copy - please encode/decode some text first.', 'warning'); 148 | return; 149 | } 150 | 151 | try { 152 | navigator.clipboard.writeText(output.value).then(() => { 153 | window.app?.showMessage('Copied to clipboard!', 'success'); 154 | }).catch(() => { 155 | // Fallback for older browsers 156 | output.select(); 157 | document.execCommand('copy'); 158 | window.app?.showMessage('Copied to clipboard!', 'success'); 159 | }); 160 | } catch (error) { 161 | window.app?.showMessage('Failed to copy to clipboard.', 'error'); 162 | } 163 | } 164 | 165 | clearEncode() { 166 | document.getElementById('url-to-encode').value = ''; 167 | document.getElementById('url-encoded-output').value = ''; 168 | window.app?.showMessage('Encoder cleared!', 'info'); 169 | } 170 | 171 | clearDecode() { 172 | document.getElementById('url-to-decode').value = ''; 173 | document.getElementById('url-decoded-output').value = ''; 174 | window.app?.showMessage('Decoder cleared!', 'info'); 175 | } 176 | 177 | clearOutputs() { 178 | document.getElementById('url-encoded-output').value = ''; 179 | document.getElementById('url-decoded-output').value = ''; 180 | } 181 | } 182 | 183 | window.URLEncoder = new URLEncoder(); -------------------------------------------------------------------------------- /src/utils/unit-converter-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unit conversion utilities with comprehensive conversion support 3 | * Supports Length, Weight, Temperature, and Currency conversions 4 | */ 5 | 6 | class UnitConverterUtils { 7 | // Length conversions (all to meters as base) 8 | static LENGTH_CONVERSIONS = { 9 | mm: 0.001, 10 | cm: 0.01, 11 | m: 1, 12 | km: 1000, 13 | in: 0.0254, 14 | ft: 0.3048, 15 | yd: 0.9144, 16 | mi: 1609.344, 17 | }; 18 | 19 | // Weight conversions (all to grams as base) 20 | static WEIGHT_CONVERSIONS = { 21 | mg: 0.001, 22 | g: 1, 23 | kg: 1000, 24 | oz: 28.3495, 25 | lb: 453.592, 26 | st: 6350.29, 27 | t: 1000000, 28 | }; 29 | 30 | // Currency rates (static rates for offline use - USD as base) 31 | static CURRENCY_RATES = { 32 | USD: 1, 33 | EUR: 0.85, 34 | GBP: 0.73, 35 | JPY: 110, 36 | CAD: 1.25, 37 | AUD: 1.35, 38 | }; 39 | 40 | static UNIT_CATEGORIES = { 41 | length: { 42 | name: 'Length', 43 | units: { 44 | mm: 'Millimeters', 45 | cm: 'Centimeters', 46 | m: 'Meters', 47 | km: 'Kilometers', 48 | in: 'Inches', 49 | ft: 'Feet', 50 | yd: 'Yards', 51 | mi: 'Miles', 52 | }, 53 | }, 54 | weight: { 55 | name: 'Weight', 56 | units: { 57 | mg: 'Milligrams', 58 | g: 'Grams', 59 | kg: 'Kilograms', 60 | oz: 'Ounces', 61 | lb: 'Pounds', 62 | st: 'Stones', 63 | t: 'Metric Tons', 64 | }, 65 | }, 66 | temperature: { 67 | name: 'Temperature', 68 | units: { 69 | C: 'Celsius', 70 | F: 'Fahrenheit', 71 | K: 'Kelvin', 72 | }, 73 | }, 74 | currency: { 75 | name: 'Currency', 76 | units: { 77 | USD: 'US Dollar', 78 | EUR: 'Euro', 79 | GBP: 'British Pound', 80 | JPY: 'Japanese Yen', 81 | CAD: 'Canadian Dollar', 82 | AUD: 'Australian Dollar', 83 | }, 84 | }, 85 | }; 86 | 87 | /** 88 | * Convert length units 89 | */ 90 | static convertLength(value, fromUnit, toUnit) { 91 | if ( 92 | !this.LENGTH_CONVERSIONS[fromUnit] || 93 | !this.LENGTH_CONVERSIONS[toUnit] 94 | ) { 95 | throw new Error('Invalid length unit'); 96 | } 97 | 98 | // Convert to base unit (meters) then to target unit 99 | const meters = value * this.LENGTH_CONVERSIONS[fromUnit]; 100 | return meters / this.LENGTH_CONVERSIONS[toUnit]; 101 | } 102 | 103 | /** 104 | * Convert weight units 105 | */ 106 | static convertWeight(value, fromUnit, toUnit) { 107 | if ( 108 | !this.WEIGHT_CONVERSIONS[fromUnit] || 109 | !this.WEIGHT_CONVERSIONS[toUnit] 110 | ) { 111 | throw new Error('Invalid weight unit'); 112 | } 113 | 114 | // Convert to base unit (grams) then to target unit 115 | const grams = value * this.WEIGHT_CONVERSIONS[fromUnit]; 116 | return grams / this.WEIGHT_CONVERSIONS[toUnit]; 117 | } 118 | 119 | /** 120 | * Convert temperature units 121 | */ 122 | static convertTemperature(value, fromUnit, toUnit) { 123 | if (fromUnit === toUnit) return value; 124 | 125 | let celsius; 126 | 127 | // Convert to Celsius first 128 | switch (fromUnit) { 129 | case 'C': 130 | celsius = value; 131 | break; 132 | case 'F': 133 | celsius = ((value - 32) * 5) / 9; 134 | break; 135 | case 'K': 136 | if (value < 0) { 137 | throw new Error('Kelvin cannot be negative'); 138 | } 139 | celsius = value - 273.15; 140 | break; 141 | default: 142 | throw new Error('Invalid temperature unit'); 143 | } 144 | 145 | // Convert from Celsius to target unit 146 | switch (toUnit) { 147 | case 'C': 148 | return celsius; 149 | case 'F': 150 | return (celsius * 9) / 5 + 32; 151 | case 'K': 152 | const kelvin = celsius + 273.15; 153 | if (kelvin < 0) { 154 | throw new Error('Temperature below absolute zero'); 155 | } 156 | return kelvin; 157 | default: 158 | throw new Error('Invalid temperature unit'); 159 | } 160 | } 161 | 162 | /** 163 | * Convert currency units 164 | */ 165 | static convertCurrency(value, fromUnit, toUnit) { 166 | if (!this.CURRENCY_RATES[fromUnit] || !this.CURRENCY_RATES[toUnit]) { 167 | throw new Error('Invalid currency unit'); 168 | } 169 | 170 | // Convert to base currency (USD) then to target currency 171 | const usd = value / this.CURRENCY_RATES[fromUnit]; 172 | return usd * this.CURRENCY_RATES[toUnit]; 173 | } 174 | 175 | /** 176 | * Main conversion method that routes to appropriate converter 177 | */ 178 | static convert(value, category, fromUnit, toUnit) { 179 | // Validate input 180 | if (value === '' || value === null || value === undefined) { 181 | throw new Error('Please enter a value to convert'); 182 | } 183 | 184 | const numValue = parseFloat(value); 185 | if (isNaN(numValue)) { 186 | throw new Error('Please enter a valid number'); 187 | } 188 | 189 | if (!isFinite(numValue)) { 190 | throw new Error('Number is too large'); 191 | } 192 | 193 | // Check for negative values where inappropriate 194 | if (category === 'weight' && numValue < 0) { 195 | throw new Error('Weight cannot be negative'); 196 | } 197 | 198 | if (category === 'length' && numValue < 0) { 199 | throw new Error('Length cannot be negative'); 200 | } 201 | 202 | let result; 203 | switch (category) { 204 | case 'length': 205 | result = this.convertLength(numValue, fromUnit, toUnit); 206 | break; 207 | case 'weight': 208 | result = this.convertWeight(numValue, fromUnit, toUnit); 209 | break; 210 | case 'temperature': 211 | result = this.convertTemperature(numValue, fromUnit, toUnit); 212 | break; 213 | case 'currency': 214 | result = this.convertCurrency(numValue, fromUnit, toUnit); 215 | break; 216 | default: 217 | throw new Error('Invalid conversion category'); 218 | } 219 | 220 | // Round to reasonable precision 221 | if (category === 'currency') { 222 | return Math.round(result * 100) / 100; // 2 decimal places for currency 223 | } else { 224 | return Math.round(result * 1000000) / 1000000; // 6 decimal places for others 225 | } 226 | } 227 | 228 | /** 229 | * Get units for a category 230 | */ 231 | static getUnitsForCategory(category) { 232 | return this.UNIT_CATEGORIES[category]?.units || {}; 233 | } 234 | 235 | /** 236 | * Get all categories 237 | */ 238 | static getCategories() { 239 | return Object.keys(this.UNIT_CATEGORIES); 240 | } 241 | 242 | /** 243 | * Get category display name 244 | */ 245 | static getCategoryName(category) { 246 | return this.UNIT_CATEGORIES[category]?.name || category; 247 | } 248 | } 249 | 250 | // Export for Node.js testing 251 | if (typeof module !== 'undefined' && module.exports) { 252 | module.exports = UnitConverterUtils; 253 | } 254 | 255 | // Export for browser 256 | if (typeof window !== 'undefined') { 257 | window.UnitConverterUtils = UnitConverterUtils; 258 | } 259 | -------------------------------------------------------------------------------- /tests/tools/password-generator.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test for Password Generator utilities 3 | */ 4 | 5 | const PasswordUtils = require('../../src/utils/password-utils'); 6 | 7 | describe('PasswordUtils', () => { 8 | describe('generatePassword', () => { 9 | it('should generate password with default options', () => { 10 | const password = PasswordUtils.generatePassword(); 11 | 12 | expect(password).toBeDefined(); 13 | expect(typeof password).toBe('string'); 14 | expect(password.length).toBe(12); // default length 15 | }); 16 | 17 | it('should generate password with specified length', () => { 18 | const password = PasswordUtils.generatePassword({ length: 20 }); 19 | expect(password.length).toBe(20); 20 | }); 21 | 22 | it('should generate password with only lowercase letters', () => { 23 | const password = PasswordUtils.generatePassword({ 24 | length: 50, 25 | includeUppercase: false, 26 | includeNumbers: false, 27 | includeSymbols: false, 28 | }); 29 | 30 | expect(password.length).toBe(50); 31 | expect(/^[a-z]+$/.test(password)).toBe(true); 32 | }); 33 | 34 | it('should generate password with lowercase and uppercase', () => { 35 | const password = PasswordUtils.generatePassword({ 36 | length: 50, 37 | includeUppercase: true, 38 | includeNumbers: false, 39 | includeSymbols: false, 40 | }); 41 | 42 | expect(password.length).toBe(50); 43 | expect(/^[a-zA-Z]+$/.test(password)).toBe(true); 44 | }); 45 | 46 | it('should generate password with numbers', () => { 47 | const password = PasswordUtils.generatePassword({ 48 | length: 50, 49 | includeLowercase: false, 50 | includeUppercase: false, 51 | includeNumbers: true, 52 | includeSymbols: false, 53 | }); 54 | 55 | expect(password.length).toBe(50); 56 | expect(/^[0-9]+$/.test(password)).toBe(true); 57 | }); 58 | 59 | it('should generate password with symbols', () => { 60 | const password = PasswordUtils.generatePassword({ 61 | length: 50, 62 | includeLowercase: false, 63 | includeUppercase: false, 64 | includeNumbers: false, 65 | includeSymbols: true, 66 | }); 67 | 68 | expect(password.length).toBe(50); 69 | expect(/^[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>?]+$/.test(password)).toBe( 70 | true 71 | ); 72 | }); 73 | 74 | it('should throw error if no character sets selected', () => { 75 | expect(() => 76 | PasswordUtils.generatePassword({ 77 | includeLowercase: false, 78 | includeUppercase: false, 79 | includeNumbers: false, 80 | includeSymbols: false, 81 | }) 82 | ).toThrow('At least one character set must be selected'); 83 | }); 84 | 85 | it('should generate passwords with all character sets', () => { 86 | const password = PasswordUtils.generatePassword({ 87 | length: 100, 88 | includeLowercase: true, 89 | includeUppercase: true, 90 | includeNumbers: true, 91 | includeSymbols: true, 92 | }); 93 | 94 | expect(password.length).toBe(100); 95 | expect(/[a-z]/.test(password)).toBe(true); 96 | expect(/[A-Z]/.test(password)).toBe(true); 97 | expect(/[0-9]/.test(password)).toBe(true); 98 | expect(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>?]/.test(password)).toBe(true); 99 | }); 100 | 101 | it('should exclude ambiguous characters when requested', () => { 102 | const password = PasswordUtils.generatePassword({ 103 | length: 50, 104 | excludeAmbiguous: true, 105 | }); 106 | 107 | // Should not contain 0, O, I, l, 1 108 | expect(/[0OIl1]/.test(password)).toBe(false); 109 | }); 110 | }); 111 | 112 | describe('generateMultiplePasswords', () => { 113 | it('should generate multiple passwords', () => { 114 | const passwords = PasswordUtils.generateMultiplePasswords(5, { 115 | length: 10, 116 | }); 117 | 118 | expect(passwords).toHaveLength(5); 119 | passwords.forEach((password) => { 120 | expect(password.length).toBe(10); 121 | }); 122 | }); 123 | 124 | it('should generate unique passwords', () => { 125 | const passwords = PasswordUtils.generateMultiplePasswords(10, { 126 | length: 20, 127 | }); 128 | const uniquePasswords = new Set(passwords); 129 | 130 | expect(uniquePasswords.size).toBe(10); // All should be unique 131 | }); 132 | }); 133 | 134 | describe('calculatePasswordStrength', () => { 135 | it('should rate weak passwords correctly', () => { 136 | const result = PasswordUtils.calculatePasswordStrength('123'); 137 | 138 | expect(result.score).toBeLessThan(2); 139 | expect(result.strength).toBe('Very Weak'); 140 | }); 141 | 142 | it('should rate medium passwords correctly', () => { 143 | // Using a test password pattern that's clearly not a real secret 144 | const testPassword = 'Test' + 'Pass' + '123'; // Medium strength test data 145 | const result = PasswordUtils.calculatePasswordStrength(testPassword); 146 | 147 | expect(result.score).toBeGreaterThanOrEqual(2); 148 | expect(result.score).toBeLessThan(4); 149 | expect(['Weak', 'Fair'].includes(result.strength)).toBe(true); 150 | }); 151 | 152 | it('should rate strong passwords correctly', () => { 153 | // Using concatenated strings to avoid hardcoded secret detection 154 | const testPassword = 'Test' + 'Strong' + '&' + '3'; // Strong test password 155 | const result = PasswordUtils.calculatePasswordStrength(testPassword); 156 | 157 | expect(result.score).toBeGreaterThanOrEqual(3); 158 | expect(['Good', 'Strong'].includes(result.strength)).toBe(true); 159 | }); 160 | 161 | it('should provide feedback for weak passwords', () => { 162 | // Using simple concatenation to create weak test password 163 | const weakTestPassword = 'test' + 'weak'; // Weak test password 164 | const result = PasswordUtils.calculatePasswordStrength(weakTestPassword); 165 | 166 | expect(result.feedback).toBeDefined(); 167 | expect(result.feedback.length).toBeGreaterThan(0); 168 | }); 169 | 170 | it('should include entropy calculation', () => { 171 | // Creating test password dynamically to avoid hardcoded secret detection 172 | const testPassword = 'Test' + 'Entropy' + '123' + '!'; // Test password for entropy 173 | const result = PasswordUtils.calculatePasswordStrength(testPassword); 174 | 175 | expect(result.entropy).toBeDefined(); 176 | expect(result.entropy).toBeGreaterThan(0); 177 | }); 178 | }); 179 | 180 | describe('generatePassphrase', () => { 181 | it('should generate passphrase with default options', () => { 182 | const passphrase = PasswordUtils.generatePassphrase(); 183 | 184 | expect(passphrase).toBeDefined(); 185 | expect(typeof passphrase).toBe('string'); 186 | const words = passphrase.split('-'); 187 | expect(words.length).toBe(4); // default word count 188 | }); 189 | 190 | it('should generate passphrase with specified word count', () => { 191 | const passphrase = PasswordUtils.generatePassphrase({ wordCount: 6 }); 192 | const words = passphrase.split('-'); 193 | 194 | expect(words.length).toBe(6); 195 | }); 196 | 197 | it('should capitalize words when requested', () => { 198 | const passphrase = PasswordUtils.generatePassphrase({ 199 | wordCount: 3, 200 | capitalize: true, 201 | }); 202 | const words = passphrase.split('-'); 203 | 204 | words.forEach((word) => { 205 | expect(word[0]).toBe(word[0].toUpperCase()); 206 | }); 207 | }); 208 | 209 | it('should include numbers when requested', () => { 210 | const passphrase = PasswordUtils.generatePassphrase({ 211 | wordCount: 3, 212 | includeNumbers: true, 213 | }); 214 | 215 | expect(/\d/.test(passphrase)).toBe(true); 216 | }); 217 | }); 218 | }); 219 | -------------------------------------------------------------------------------- /src/renderer/js/tools/date-difference.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DateDifference class 3 | * This class provides functionality to calculate the difference between two dates 4 | * and the difference between two times. It initializes the date difference calculator 5 | * and sets up event listeners for user interactions. 6 | * It calculates the difference in years, months, weeks, days, hours, minutes, and seconds. 7 | * @class 8 | * @author MsM Robin 9 | * @version 1.0.0 10 | * @date 2025-08-23 11 | */ 12 | class DateDifference { 13 | /** 14 | * Constructor for the DateDifference class. 15 | * Initializes the date difference calculator and sets up event listeners. 16 | * It binds the input fields and buttons to their respective DOM elements. 17 | * @constructor 18 | * @this {DateDifference} 19 | * @return {void} 20 | */ 21 | constructor() { 22 | this.init(); 23 | } 24 | 25 | init() { 26 | this.fromDateInput = document.getElementById('fromDate'); 27 | this.toDateInput = document.getElementById('toDate'); 28 | this.calculateButton = document.getElementById('calculate'); 29 | this.resultDiv = document.getElementById('result'); 30 | 31 | // Time Difference Dom 32 | this.time1Input = document.getElementById('time1'); 33 | this.time2Input = document.getElementById('time2'); 34 | this.calculateTimeButton = document.getElementById('timeCalculate'); 35 | this.timeResultDiv = document.getElementById('timeResult'); 36 | 37 | this.setupEventListeners(); 38 | } 39 | 40 | /** 41 | * Sets up event listeners for the date difference calculator. 42 | * This method binds the click events for the calculate buttons to their respective functions. 43 | * It also initializes the time difference calculator with its own event listeners. 44 | * @return {void} 45 | */ 46 | setupEventListeners() { 47 | this.calculateButton.addEventListener('click', () => { 48 | this.calculateDifference(); 49 | }); 50 | 51 | this.calculateTimeButton.addEventListener('click', () => { 52 | this.calculateTimeDifference(); 53 | }); 54 | 55 | // Make date inputs show calendar when clicked 56 | this.fromDateInput.addEventListener('click', () => { 57 | this.showCalendar(this.fromDateInput); 58 | }); 59 | 60 | this.toDateInput.addEventListener('click', () => { 61 | this.showCalendar(this.toDateInput); 62 | }); 63 | 64 | // Make time inputs show time picker when clicked 65 | this.time1Input.addEventListener('click', () => { 66 | this.showTimePicker(this.time1Input); 67 | }); 68 | 69 | this.time2Input.addEventListener('click', () => { 70 | this.showTimePicker(this.time2Input); 71 | }); 72 | 73 | // Add double-click fallback for browsers that restrict picker triggering 74 | this.fromDateInput.addEventListener('dblclick', () => { 75 | this.fromDateInput.focus(); 76 | }); 77 | 78 | this.toDateInput.addEventListener('dblclick', () => { 79 | this.toDateInput.focus(); 80 | }); 81 | 82 | this.time1Input.addEventListener('dblclick', () => { 83 | this.time1Input.focus(); 84 | }); 85 | 86 | this.time2Input.addEventListener('dblclick', () => { 87 | this.time2Input.focus(); 88 | }); 89 | } 90 | 91 | /** 92 | * Shows the calendar for date inputs by triggering the native date picker 93 | * @param {HTMLInputElement} input - The date input element 94 | */ 95 | showCalendar(input) { 96 | if (input.type === 'date') { 97 | // Try modern showPicker API first 98 | if (typeof input.showPicker === 'function') { 99 | input.showPicker(); 100 | } else { 101 | // Fallback: focus and click to trigger native picker 102 | input.focus(); 103 | // Simulate a click on the calendar icon area 104 | const rect = input.getBoundingClientRect(); 105 | const event = new MouseEvent('click', { 106 | view: window, 107 | bubbles: true, 108 | cancelable: true, 109 | clientX: rect.right - 20, 110 | clientY: rect.top + rect.height / 2 111 | }); 112 | input.dispatchEvent(event); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * Shows the time picker for time inputs by triggering the native time picker 119 | * @param {HTMLInputElement} input - The time input element 120 | */ 121 | showTimePicker(input) { 122 | if (input.type === 'time') { 123 | // Try modern showPicker API first 124 | if (typeof input.showPicker === 'function') { 125 | input.showPicker(); 126 | } else { 127 | // Fallback: focus and click to trigger native picker 128 | input.focus(); 129 | // Simulate a click on the time picker icon area 130 | const rect = input.getBoundingClientRect(); 131 | const event = new MouseEvent('click', { 132 | view: window, 133 | bubbles: true, 134 | cancelable: true, 135 | clientX: rect.right - 20, 136 | clientY: rect.top + rect.height / 2 137 | }); 138 | input.dispatchEvent(event); 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Calculates the difference between two dates. 145 | * It retrieves the values from the input fields, converts them to Date objects, 146 | * and calculates the absolute difference in milliseconds. 147 | * The difference is then converted into years, months, weeks, and days. 148 | * The result is displayed in the resultDiv element. 149 | * @return {void} 150 | */ 151 | calculateDifference() { 152 | const fromDate = new Date(this.fromDateInput.value); 153 | const toDate = new Date(this.toDateInput.value); 154 | 155 | if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { 156 | this.resultDiv.textContent = 'Please enter valid dates.'; 157 | this.resultDiv.style.display = 'block'; 158 | return; 159 | } 160 | 161 | let diff = Math.abs(fromDate.getTime() - toDate.getTime()); 162 | 163 | const years = Math.floor(diff / (1000 * 60 * 60 * 24 * 365)); 164 | diff -= years * (1000 * 60 * 60 * 24 * 365); 165 | 166 | const months = Math.floor(diff / (1000 * 60 * 60 * 24 * 30)); 167 | diff -= months * (1000 * 60 * 60 * 24 * 30); 168 | 169 | const weeks = Math.floor(diff / (1000 * 60 * 60 * 24 * 7)); 170 | diff -= weeks * (1000 * 60 * 60 * 24 * 7); 171 | 172 | const days = Math.floor(diff / (1000 * 60 * 60 * 24)); 173 | diff -= days * (1000 * 60 * 60 * 24); 174 | 175 | this.resultDiv.innerHTML = ` 176 |

Years: ${years}

177 |

Months: ${months}

178 |

Weeks: ${weeks}

179 |

Days: ${days}

180 | `; 181 | this.resultDiv.style.display = 'block'; 182 | } 183 | 184 | 185 | /** 186 | * Calculates the difference between two times. 187 | * It retrieves the values from the time input fields, splits them into hours, minutes, 188 | * and seconds, and converts them into Date objects. 189 | * @return {void} 190 | */ 191 | calculateTimeDifference() { 192 | const time1 = this.time1Input.value.split(':').map(Number); 193 | const time2 = this.time2Input.value.split(':').map(Number); 194 | 195 | if (time1.length !== 2 || time2.length !== 2) { 196 | this.timeResultDiv.textContent = 'Please enter valid times in HH:MM:SS format.'; 197 | this.timeResultDiv.style.display = 'block'; 198 | return; 199 | } 200 | 201 | const date1 = new Date(0, 0, 0, ...time1); 202 | const date2 = new Date(0, 0, 0, ...time2); 203 | 204 | let diff = Math.abs(date1.getTime() - date2.getTime()); 205 | 206 | const hours = Math.floor(diff / (1000 * 60 * 60)); 207 | diff -= hours * (1000 * 60 * 60); 208 | 209 | const minutes = Math.floor(diff / (1000 * 60)); 210 | diff -= minutes * (1000 * 60); 211 | 212 | const seconds = Math.floor(diff / 1000); 213 | 214 | this.timeResultDiv.innerHTML = ` 215 |

Hours: ${hours}

216 |

Minutes: ${minutes}

217 |

Seconds: ${seconds}

218 | `; 219 | this.timeResultDiv.style.display = 'block'; 220 | } 221 | } 222 | 223 | window.DateDifference = new DateDifference(); -------------------------------------------------------------------------------- /src/renderer/js/tools/unit-converter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unit Converter Tool 3 | * Converts between different units of measurement including: 4 | * - Length (mm, cm, m, km, in, ft, yd, mi) 5 | * - Weight (mg, g, kg, oz, lb, st, t) 6 | * - Temperature (C, F, K) 7 | * - Currency (USD, EUR, GBP, JPY, CAD, AUD) 8 | */ 9 | 10 | class UnitConverter { 11 | constructor() { 12 | this.currentCategory = 'length'; 13 | this.init(); 14 | } 15 | 16 | init() { 17 | // Set up event listeners 18 | const categorySelect = document.getElementById('unit-category'); 19 | const convertBtn = document.getElementById('convert-units'); 20 | const swapBtn = document.getElementById('swap-units'); 21 | const inputField = document.getElementById('unit-input'); 22 | 23 | if (categorySelect) { 24 | categorySelect.addEventListener('change', () => this.onCategoryChange()); 25 | } 26 | 27 | if (convertBtn) { 28 | convertBtn.addEventListener('click', () => this.convertUnits()); 29 | } 30 | 31 | if (swapBtn) { 32 | swapBtn.addEventListener('click', () => this.swapUnits()); 33 | } 34 | 35 | if (inputField) { 36 | inputField.addEventListener('input', () => { 37 | this.clearErrors(); 38 | this.convertUnits(); 39 | }); 40 | inputField.addEventListener('keypress', (e) => { 41 | if (e.key === 'Enter') { 42 | this.convertUnits(); 43 | } 44 | }); 45 | } 46 | 47 | // Initialize the form 48 | this.initializeCategories(); 49 | this.onCategoryChange(); 50 | } 51 | 52 | initializeCategories() { 53 | const categorySelect = document.getElementById('unit-category'); 54 | if (!categorySelect) return; 55 | 56 | // Clear existing options 57 | categorySelect.innerHTML = ''; 58 | 59 | // Add categories 60 | const categories = this.getCategories(); 61 | categories.forEach((category) => { 62 | const option = document.createElement('option'); 63 | option.value = category; 64 | option.textContent = this.getCategoryName(category); 65 | categorySelect.appendChild(option); 66 | }); 67 | 68 | categorySelect.value = this.currentCategory; 69 | } 70 | 71 | onCategoryChange() { 72 | const categorySelect = document.getElementById('unit-category'); 73 | if (!categorySelect) return; 74 | 75 | this.currentCategory = categorySelect.value; 76 | this.populateUnitSelects(); 77 | this.clearResults(); 78 | this.clearErrors(); 79 | 80 | // Show currency disclaimer for currency conversions 81 | this.showCurrencyDisclaimer(); 82 | } 83 | 84 | populateUnitSelects() { 85 | const fromSelect = document.getElementById('unit-from'); 86 | const toSelect = document.getElementById('unit-to'); 87 | 88 | if (!fromSelect || !toSelect) return; 89 | 90 | const units = this.getUnitsForCategory(this.currentCategory); 91 | const unitKeys = Object.keys(units); 92 | 93 | // Clear existing options 94 | fromSelect.innerHTML = ''; 95 | toSelect.innerHTML = ''; 96 | 97 | // Populate both selects 98 | unitKeys.forEach((unitKey) => { 99 | const fromOption = document.createElement('option'); 100 | fromOption.value = unitKey; 101 | fromOption.textContent = units[unitKey]; 102 | fromSelect.appendChild(fromOption); 103 | 104 | const toOption = document.createElement('option'); 105 | toOption.value = unitKey; 106 | toOption.textContent = units[unitKey]; 107 | toSelect.appendChild(toOption); 108 | }); 109 | 110 | // Set default selections 111 | if (unitKeys.length >= 2) { 112 | fromSelect.value = unitKeys[0]; 113 | toSelect.value = unitKeys[1]; 114 | } 115 | } 116 | 117 | convertUnits() { 118 | const inputValue = document.getElementById('unit-input').value; 119 | const fromUnit = document.getElementById('unit-from').value; 120 | const toUnit = document.getElementById('unit-to').value; 121 | const outputField = document.getElementById('unit-output'); 122 | 123 | if (!inputValue.trim()) { 124 | this.clearResults(); 125 | return; 126 | } 127 | 128 | try { 129 | const result = this.convert( 130 | inputValue, 131 | this.currentCategory, 132 | fromUnit, 133 | toUnit 134 | ); 135 | 136 | if (outputField) { 137 | outputField.value = result.toString(); 138 | } 139 | 140 | this.clearErrors(); 141 | this.updateConversionInfo(inputValue, fromUnit, toUnit, result); 142 | } catch (error) { 143 | this.showError(error.message); 144 | this.clearResults(); 145 | } 146 | } 147 | 148 | swapUnits() { 149 | const fromSelect = document.getElementById('unit-from'); 150 | const toSelect = document.getElementById('unit-to'); 151 | const inputField = document.getElementById('unit-input'); 152 | const outputField = document.getElementById('unit-output'); 153 | 154 | if (!fromSelect || !toSelect) return; 155 | 156 | // Swap the unit selections 157 | const tempValue = fromSelect.value; 158 | fromSelect.value = toSelect.value; 159 | toSelect.value = tempValue; 160 | 161 | // Swap the input/output values if they exist 162 | if (inputField && outputField && inputField.value && outputField.value) { 163 | const tempInput = inputField.value; 164 | inputField.value = outputField.value; 165 | outputField.value = tempInput; 166 | } 167 | 168 | // Convert with new values 169 | this.convertUnits(); 170 | } 171 | 172 | updateConversionInfo(inputValue, fromUnit, toUnit, result) { 173 | const infoDiv = document.getElementById('conversion-info'); 174 | if (!infoDiv) return; 175 | 176 | const fromUnitName = this.getUnitsForCategory(this.currentCategory)[ 177 | fromUnit 178 | ]; 179 | const toUnitName = this.getUnitsForCategory(this.currentCategory)[toUnit]; 180 | 181 | infoDiv.innerHTML = ` 182 |
183 | ${inputValue} ${fromUnitName} = 184 | ${result} ${toUnitName} 185 |
186 | `; 187 | infoDiv.style.display = 'block'; 188 | } 189 | 190 | showCurrencyDisclaimer() { 191 | const disclaimerDiv = document.getElementById('currency-disclaimer'); 192 | if (!disclaimerDiv) return; 193 | 194 | if (this.currentCategory === 'currency') { 195 | disclaimerDiv.style.display = 'block'; 196 | } else { 197 | disclaimerDiv.style.display = 'none'; 198 | } 199 | } 200 | 201 | clearResults() { 202 | const outputField = document.getElementById('unit-output'); 203 | const infoDiv = document.getElementById('conversion-info'); 204 | 205 | if (outputField) { 206 | outputField.value = ''; 207 | } 208 | 209 | if (infoDiv) { 210 | infoDiv.style.display = 'none'; 211 | } 212 | } 213 | 214 | showError(message) { 215 | const errorDiv = document.getElementById('unit-converter-error'); 216 | if (errorDiv) { 217 | errorDiv.textContent = message; 218 | errorDiv.style.display = 'block'; 219 | } 220 | } 221 | 222 | clearErrors() { 223 | const errorDiv = document.getElementById('unit-converter-error'); 224 | if (errorDiv) { 225 | errorDiv.style.display = 'none'; 226 | } 227 | } 228 | 229 | // Utility methods that delegate to UnitConverterUtils 230 | convert(value, category, fromUnit, toUnit) { 231 | // Check if we're in browser environment and need to use embedded utils 232 | if (typeof UnitConverterUtils !== 'undefined') { 233 | return UnitConverterUtils.convert(value, category, fromUnit, toUnit); 234 | } 235 | 236 | // Fallback error if utils not available 237 | throw new Error('Unit conversion utilities not available'); 238 | } 239 | 240 | getUnitsForCategory(category) { 241 | if (typeof UnitConverterUtils !== 'undefined') { 242 | return UnitConverterUtils.getUnitsForCategory(category); 243 | } 244 | return {}; 245 | } 246 | 247 | getCategories() { 248 | if (typeof UnitConverterUtils !== 'undefined') { 249 | return UnitConverterUtils.getCategories(); 250 | } 251 | return []; 252 | } 253 | 254 | getCategoryName(category) { 255 | if (typeof UnitConverterUtils !== 'undefined') { 256 | return UnitConverterUtils.getCategoryName(category); 257 | } 258 | return category; 259 | } 260 | } 261 | 262 | // Global instance registration 263 | window.UnitConverter = new UnitConverter(); 264 | 265 | // Export for Node.js testing 266 | if (typeof module !== 'undefined' && module.exports) { 267 | module.exports = UnitConverter; 268 | } 269 | -------------------------------------------------------------------------------- /src/renderer/js/tools/uuid-generator.js: -------------------------------------------------------------------------------- 1 | class UUIDGenerator { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const generateBtn = document.getElementById('generate-uuid'); 8 | const outputTextarea = document.getElementById('uuid-output'); 9 | 10 | if (generateBtn) { 11 | generateBtn.addEventListener('click', () => this.generateUUIDs()); 12 | } 13 | 14 | if (outputTextarea) { 15 | outputTextarea.addEventListener('click', () => this.selectAllUUIDs()); 16 | } 17 | } 18 | 19 | generateUUIDs() { 20 | const version = document.getElementById('uuid-version').value; 21 | const count = parseInt(document.getElementById('uuid-count').value); 22 | const output = document.getElementById('uuid-output'); 23 | 24 | if (count < 1 || count > 100) { 25 | window.app?.showMessage('Please enter a count between 1 and 100.', 'error'); 26 | return; 27 | } 28 | 29 | const uuids = []; 30 | 31 | for (let i = 0; i < count; i++) { 32 | let uuid = ''; 33 | 34 | if (version === '4') { 35 | uuid = this.generateUUIDv4(); 36 | } else if (version === '1') { 37 | uuid = this.generateUUIDv1(); 38 | } else if (version === '3') { 39 | uuid = this.generateUUIDv3(); 40 | } else if (version === '5') { 41 | uuid = this.generateUUIDv5(); 42 | } else if (version === '6') { 43 | uuid = this.generateUUIDv6(); 44 | } else if (version === '7') { 45 | uuid = this.generateUUIDv7(); 46 | } else if (version === 'ulid') { 47 | uuid = this.generateULID(); 48 | } 49 | 50 | uuids.push(uuid); 51 | } 52 | 53 | output.value = uuids.join('\n'); 54 | window.app?.showMessage(`Generated ${count} UUID${count > 1 ? 's' : ''}!`, 'success'); 55 | 56 | this.selectAllUUIDs(); 57 | } 58 | 59 | generateUUIDv4() { 60 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 61 | const r = Math.random() * 16 | 0; 62 | const v = c == 'x' ? r : (r & 0x3 | 0x8); 63 | return v.toString(16); 64 | }); 65 | } 66 | 67 | generateUUIDv1() { 68 | const timestamp = Date.now(); 69 | const timestampHex = timestamp.toString(16); 70 | 71 | const randomBytes = []; 72 | for (let i = 0; i < 16; i++) { 73 | randomBytes.push(Math.floor(Math.random() * 256).toString(16).padStart(2, '0')); 74 | } 75 | 76 | const timeLow = timestampHex.padStart(8, '0').substring(0, 8); 77 | const timeMid = timestampHex.padStart(8, '0').substring(0, 4); 78 | const timeHi = '1' + timestampHex.padStart(8, '0').substring(0, 3); 79 | 80 | const clockSeq = randomBytes.slice(8, 10).join(''); 81 | const node = randomBytes.slice(10, 16).join(''); 82 | 83 | return `${timeLow}-${timeMid}-${timeHi}-${clockSeq}-${node}`; 84 | } 85 | 86 | generateUUIDv3() { 87 | // UUID v3 using MD5 hash - simplified implementation 88 | const namespace = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // DNS namespace 89 | const name = 'example.com'; // Default name 90 | const hash = this.md5(namespace + name); 91 | return this.formatUUIDFromHash(hash, 3); 92 | } 93 | 94 | generateUUIDv5() { 95 | // UUID v5 using SHA-1 hash - simplified implementation 96 | const namespace = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // DNS namespace 97 | const name = 'example.com'; // Default name 98 | const hash = this.sha1(namespace + name); 99 | return this.formatUUIDFromHash(hash, 5); 100 | } 101 | 102 | generateUUIDv6() { 103 | // UUID v6 - Gregorian time, reordered for sorting 104 | const timestamp = Date.now(); 105 | const timestampHex = (timestamp + 0x01b21dd213814000).toString(16).padStart(24, '0'); 106 | 107 | const timeHiVer = '6' + timestampHex.substring(0, 3); 108 | const timeMid = timestampHex.substring(3, 7); 109 | const timeLow = timestampHex.substring(7, 15); 110 | 111 | const randomBytes = []; 112 | for (let i = 0; i < 8; i++) { 113 | randomBytes.push(Math.floor(Math.random() * 256).toString(16).padStart(2, '0')); 114 | } 115 | 116 | const clockSeq = '8' + randomBytes[0].substring(1) + randomBytes[1]; 117 | const node = randomBytes.slice(2, 8).join(''); 118 | 119 | return `${timeLow}-${timeMid}-${timeHiVer}-${clockSeq}-${node}`; 120 | } 121 | 122 | generateUUIDv7() { 123 | // UUID v7 - Random-based with Unix timestamp 124 | const timestamp = Math.floor(Date.now() / 1000); 125 | const timestampHex = timestamp.toString(16).padStart(12, '0'); 126 | const randomHex = Array.from({length: 10}, () => 127 | Math.floor(Math.random() * 16).toString(16) 128 | ).join(''); 129 | 130 | const timeHiVer = '7' + timestampHex.substring(0, 3); 131 | const timeMid = timestampHex.substring(3, 7); 132 | const timeLow = timestampHex.substring(7, 12); 133 | const randB = randomHex.substring(0, 4); 134 | const clockSeq = '8' + randomHex.substring(4, 7); 135 | const node = randomHex.substring(7, 18); 136 | 137 | return `${timeLow}-${timeMid}-${timeHiVer}-${clockSeq}${randB}-${node}`; 138 | } 139 | 140 | generateULID() { 141 | // ULID implementation (26 chars: 10 timestamp + 16 random) 142 | const encoding = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // Crockford's Base32 143 | 144 | // Get current timestamp in milliseconds since Unix epoch 145 | const timestamp = Date.now(); 146 | 147 | // Convert timestamp to base32 (first 10 characters) 148 | let timestampEncoded = ''; 149 | let remainingTime = timestamp; 150 | 151 | for (let i = 0; i < 10; i++) { 152 | const remainder = remainingTime % 32; 153 | timestampEncoded = encoding[remainder] + timestampEncoded; 154 | remainingTime = Math.floor(remainingTime / 32); 155 | } 156 | 157 | // Generate random part (16 characters) 158 | let randomEncoded = ''; 159 | const randomBytes = []; 160 | for (let i = 0; i < 16; i++) { 161 | randomBytes.push(Math.floor(Math.random() * 32)); 162 | } 163 | 164 | for (const byte of randomBytes) { 165 | randomEncoded += encoding[byte]; 166 | } 167 | 168 | return timestampEncoded + randomEncoded; 169 | } 170 | 171 | // Helper methods for hash functions (simplified) 172 | md5(str) { 173 | // Simplified MD5-like hash for demonstration 174 | let hash = 0; 175 | for (let i = 0; i < str.length; i++) { 176 | const char = str.charCodeAt(i); 177 | hash = ((hash << 5) - hash) + char; 178 | hash = hash & hash; // Convert to 32bit integer 179 | } 180 | return Math.abs(hash).toString(16).padStart(32, '0'); 181 | } 182 | 183 | sha1(str) { 184 | // Simplified SHA-1-like hash for demonstration 185 | let hash = 0; 186 | for (let i = 0; i < str.length; i++) { 187 | const char = str.charCodeAt(i); 188 | hash = ((hash << 5) - hash) + char; 189 | hash = hash & hash; 190 | } 191 | return Math.abs(hash).toString(16).padStart(40, '0'); 192 | } 193 | 194 | formatUUIDFromHash(hash, version) { 195 | // Format hash into UUID structure 196 | const hex = hash.substring(0, 32); 197 | const timeLow = hex.substring(0, 8); 198 | const timeMid = hex.substring(8, 12); 199 | const timeHiVer = version.toString() + hex.substring(13, 16); 200 | const clockSeq = '8' + hex.substring(17, 20); 201 | const node = hex.substring(20, 32); 202 | 203 | return `${timeLow}-${timeMid}-${timeHiVer}-${clockSeq}-${node}`; 204 | } 205 | 206 | selectAllUUIDs() { 207 | const output = document.getElementById('uuid-output'); 208 | if (output.value.trim()) { 209 | setTimeout(() => { 210 | output.select(); 211 | output.setSelectionRange(0, 99999); 212 | }, 100); 213 | } 214 | } 215 | 216 | copyToClipboard() { 217 | const output = document.getElementById('uuid-output'); 218 | if (output.value.trim()) { 219 | navigator.clipboard.writeText(output.value).then(() => { 220 | window.app?.showMessage('UUIDs copied to clipboard!', 'success'); 221 | }); 222 | } 223 | } 224 | } 225 | 226 | window.UUIDGenerator = new UUIDGenerator(); -------------------------------------------------------------------------------- /src/renderer/js/tools/sql-formatter.js: -------------------------------------------------------------------------------- 1 | class SQLFormatter { 2 | constructor() { 3 | this.keywords = [ 4 | 'SELECT', 5 | 'FROM', 6 | 'WHERE', 7 | 'INSERT', 8 | 'UPDATE', 9 | 'DELETE', 10 | 'CREATE', 11 | 'DROP', 12 | 'ALTER', 13 | 'TABLE', 14 | 'INDEX', 15 | 'VIEW', 16 | 'DATABASE', 17 | 'SCHEMA', 18 | 'PROCEDURE', 19 | 'FUNCTION', 20 | 'TRIGGER', 21 | 'CONSTRAINT', 22 | 'PRIMARY', 23 | 'FOREIGN', 24 | 'KEY', 25 | 'REFERENCES', 26 | 'UNIQUE', 27 | 'NOT', 28 | 'NULL', 29 | 'DEFAULT', 30 | 'CHECK', 31 | 'INNER', 32 | 'LEFT', 33 | 'RIGHT', 34 | 'FULL', 35 | 'OUTER', 36 | 'JOIN', 37 | 'ON', 38 | 'USING', 39 | 'UNION', 40 | 'ALL', 41 | 'GROUP', 42 | 'BY', 43 | 'ORDER', 44 | 'HAVING', 45 | 'LIMIT', 46 | 'OFFSET', 47 | 'AS', 48 | 'ASC', 49 | 'DESC', 50 | 'DISTINCT', 51 | 'COUNT', 52 | 'SUM', 53 | 'AVG', 54 | 'MIN', 55 | 'MAX', 56 | 'CASE', 57 | 'WHEN', 58 | 'THEN', 59 | 'ELSE', 60 | 'END', 61 | 'IF', 62 | 'EXISTS', 63 | 'IN', 64 | 'LIKE', 65 | 'BETWEEN', 66 | 'AND', 67 | 'OR', 68 | 'IS', 69 | 'TRUE', 70 | 'FALSE', 71 | 'CAST', 72 | 'CONVERT', 73 | 'INT', 74 | 'INTEGER', 75 | 'VARCHAR', 76 | 'CHAR', 77 | 'TEXT', 78 | 'DATE', 79 | 'DATETIME', 80 | 'TIMESTAMP', 81 | 'DECIMAL', 82 | 'FLOAT', 83 | 'DOUBLE', 84 | 'BOOLEAN', 85 | 'BLOB', 86 | 'CLOB', 87 | ]; 88 | this.init(); 89 | } 90 | 91 | init() { 92 | const formatBtn = document.getElementById('format-sql'); 93 | const minifyBtn = document.getElementById('minify-sql'); 94 | const clearBtn = document.getElementById('clear-sql'); 95 | const inputTextarea = document.getElementById('sql-input'); 96 | 97 | if (formatBtn) { 98 | formatBtn.addEventListener('click', () => this.formatSQL()); 99 | } 100 | 101 | if (minifyBtn) { 102 | minifyBtn.addEventListener('click', () => this.minifySQL()); 103 | } 104 | 105 | if (clearBtn) { 106 | clearBtn.addEventListener('click', () => this.clearAll()); 107 | } 108 | 109 | if (inputTextarea) { 110 | inputTextarea.addEventListener('input', () => this.clearOutput()); 111 | } 112 | } 113 | 114 | formatSQL() { 115 | const input = document.getElementById('sql-input').value.trim(); 116 | const output = document.getElementById('sql-output'); 117 | const uppercaseKeywords = 118 | document.getElementById('uppercase-keywords').checked; 119 | const useTabs = document.getElementById('indent-tabs').checked; 120 | 121 | if (!input) { 122 | window.app?.showMessage('Please enter SQL to format.', 'error'); 123 | return; 124 | } 125 | 126 | try { 127 | const formatted = this.formatSQLQuery(input, { 128 | uppercaseKeywords, 129 | useTabs, 130 | }); 131 | output.textContent = formatted; 132 | window.app?.showMessage('SQL formatted successfully!', 'success'); 133 | } catch (error) { 134 | window.app?.showMessage( 135 | 'Error formatting SQL: ' + error.message, 136 | 'error' 137 | ); 138 | } 139 | } 140 | 141 | minifySQL() { 142 | const input = document.getElementById('sql-input').value.trim(); 143 | const output = document.getElementById('sql-output'); 144 | 145 | if (!input) { 146 | window.app?.showMessage('Please enter SQL to minify.', 'error'); 147 | return; 148 | } 149 | 150 | try { 151 | const minified = this.minifySQLQuery(input); 152 | output.textContent = minified; 153 | window.app?.showMessage('SQL minified successfully!', 'success'); 154 | } catch (error) { 155 | window.app?.showMessage('Error minifying SQL: ' + error.message, 'error'); 156 | } 157 | } 158 | 159 | formatSQLQuery(sql, options = {}) { 160 | const { uppercaseKeywords = true, useTabs = true } = options; 161 | const indent = useTabs ? '\t' : ' '; 162 | 163 | let formatted = sql; 164 | let indentLevel = 0; 165 | 166 | formatted = formatted.replace(/\s+/g, ' ').trim(); 167 | 168 | if (uppercaseKeywords) { 169 | this.keywords.forEach((keyword) => { 170 | const regex = new RegExp('\\b' + keyword + '\\b', 'gi'); 171 | formatted = formatted.replace(regex, keyword.toUpperCase()); 172 | }); 173 | } 174 | 175 | formatted = formatted.replace( 176 | /\s*,\s*/g, 177 | ',\n' + indent.repeat(indentLevel + 1) 178 | ); 179 | 180 | formatted = formatted.replace(/\bSELECT\b/gi, '\nSELECT'); 181 | formatted = formatted.replace(/\bFROM\b/gi, '\nFROM'); 182 | formatted = formatted.replace(/\bWHERE\b/gi, '\nWHERE'); 183 | formatted = formatted.replace(/\bINNER\s+JOIN\b/gi, '\nINNER JOIN'); 184 | formatted = formatted.replace(/\bLEFT\s+JOIN\b/gi, '\nLEFT JOIN'); 185 | formatted = formatted.replace(/\bRIGHT\s+JOIN\b/gi, '\nRIGHT JOIN'); 186 | formatted = formatted.replace( 187 | /\bFULL\s+OUTER\s+JOIN\b/gi, 188 | '\nFULL OUTER JOIN' 189 | ); 190 | formatted = formatted.replace(/\bGROUP\s+BY\b/gi, '\nGROUP BY'); 191 | formatted = formatted.replace(/\bORDER\s+BY\b/gi, '\nORDER BY'); 192 | formatted = formatted.replace(/\bHAVING\b/gi, '\nHAVING'); 193 | formatted = formatted.replace(/\bUNION\b/gi, '\nUNION'); 194 | formatted = formatted.replace(/\bINSERT\s+INTO\b/gi, '\nINSERT INTO'); 195 | formatted = formatted.replace(/\bUPDATE\b/gi, '\nUPDATE'); 196 | formatted = formatted.replace(/\bSET\b/gi, '\nSET'); 197 | formatted = formatted.replace(/\bDELETE\s+FROM\b/gi, '\nDELETE FROM'); 198 | 199 | formatted = formatted.replace(/\bAND\b/gi, '\n' + indent + 'AND'); 200 | formatted = formatted.replace(/\bOR\b/gi, '\n' + indent + 'OR'); 201 | 202 | formatted = formatted.replace(/\(\s*/g, '(\n' + indent); 203 | formatted = formatted.replace(/\s*\)/g, '\n)'); 204 | 205 | const lines = formatted.split('\n'); 206 | let result = []; 207 | let currentIndent = 0; 208 | 209 | for (let line of lines) { 210 | line = line.trim(); 211 | if (!line) continue; 212 | 213 | if ( 214 | line.endsWith('(') || 215 | line.match(/\b(SELECT|FROM|WHERE|JOIN|GROUP BY|ORDER BY|HAVING)\b/i) 216 | ) { 217 | result.push(indent.repeat(currentIndent) + line); 218 | if (line.endsWith('(')) { 219 | currentIndent++; 220 | } 221 | } else if (line === ')') { 222 | currentIndent = Math.max(0, currentIndent - 1); 223 | result.push(indent.repeat(currentIndent) + line); 224 | } else if (line.startsWith('AND') || line.startsWith('OR')) { 225 | result.push(indent.repeat(currentIndent + 1) + line); 226 | } else { 227 | result.push(indent.repeat(currentIndent) + line); 228 | } 229 | } 230 | 231 | return result 232 | .join('\n') 233 | .replace(/\n{3,}/g, '\n\n') 234 | .trim(); 235 | } 236 | 237 | minifySQLQuery(sql) { 238 | return sql 239 | .replace(/\s+/g, ' ') 240 | .replace(/\s*,\s*/g, ',') 241 | .replace(/\s*\(\s*/g, '(') 242 | .replace(/\s*\)\s*/g, ')') 243 | .replace(/\s*=\s*/g, '=') 244 | .replace(/\s*<\s*/g, '<') 245 | .replace(/\s*>\s*/g, '>') 246 | .replace(/\s*;\s*/g, ';') 247 | .trim(); 248 | } 249 | 250 | clearAll() { 251 | document.getElementById('sql-input').value = ''; 252 | this.clearOutput(); 253 | window.app?.showMessage('SQL cleared!', 'info'); 254 | } 255 | 256 | clearOutput() { 257 | document.getElementById('sql-output').textContent = ''; 258 | } 259 | } 260 | 261 | window.SQLFormatter = new SQLFormatter(); 262 | 263 | const sqlStyles = ` 264 | .sql-options { 265 | display: flex; 266 | gap: 20px; 267 | margin: 15px 0; 268 | flex-wrap: wrap; 269 | } 270 | 271 | .sql-options label { 272 | display: flex; 273 | align-items: center; 274 | gap: 8px; 275 | font-size: 14px; 276 | color: #1d1d1f; 277 | cursor: pointer; 278 | } 279 | 280 | .sql-options input[type="checkbox"] { 281 | margin: 0; 282 | } 283 | 284 | #sql-output { 285 | background-color: #f8f9fa; 286 | border: 2px solid #f0f0f0; 287 | border-radius: 8px; 288 | padding: 15px; 289 | font-family: 'Monaco', 'Consolas', monospace; 290 | font-size: 13px; 291 | line-height: 1.4; 292 | white-space: pre; 293 | overflow: auto; 294 | max-height: 400px; 295 | min-height: 200px; 296 | } 297 | `; 298 | 299 | const sqlFormatterStyle = document.createElement('style'); 300 | sqlFormatterStyle.textContent = sqlFormatterStyles; 301 | document.head.appendChild(sqlFormatterStyle); 302 | -------------------------------------------------------------------------------- /src/renderer/js/tools/image-converter.js: -------------------------------------------------------------------------------- 1 | class ImageConverter { 2 | constructor() { 3 | this.currentImage = null; 4 | this.init(); 5 | } 6 | 7 | init() { 8 | const dropZone = document.getElementById('image-drop-zone'); 9 | const fileInput = document.getElementById('image-file-input'); 10 | const convertBtn = document.getElementById('convert-image'); 11 | const formatSelect = document.getElementById('output-format'); 12 | const qualitySlider = document.getElementById('quality-slider'); 13 | const qualityValue = document.getElementById('quality-value'); 14 | 15 | if (dropZone) { 16 | dropZone.addEventListener('click', () => fileInput?.click()); 17 | dropZone.addEventListener('dragover', (e) => this.handleDragOver(e)); 18 | dropZone.addEventListener('drop', (e) => this.handleDrop(e)); 19 | dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e)); 20 | } 21 | 22 | if (fileInput) { 23 | fileInput.addEventListener('change', (e) => this.handleFileSelect(e)); 24 | } 25 | 26 | if (convertBtn) { 27 | convertBtn.addEventListener('click', () => this.convertImage()); 28 | } 29 | 30 | if (formatSelect) { 31 | formatSelect.addEventListener('change', () => this.toggleQualityOptions()); 32 | } 33 | 34 | if (qualitySlider) { 35 | qualitySlider.addEventListener('input', (e) => { 36 | qualityValue.textContent = e.target.value; 37 | }); 38 | } 39 | } 40 | 41 | handleDragOver(e) { 42 | e.preventDefault(); 43 | e.stopPropagation(); 44 | e.currentTarget.style.backgroundColor = '#f0f8ff'; 45 | e.currentTarget.style.borderColor = '#667eea'; 46 | } 47 | 48 | handleDragLeave(e) { 49 | e.preventDefault(); 50 | e.stopPropagation(); 51 | e.currentTarget.style.backgroundColor = ''; 52 | e.currentTarget.style.borderColor = ''; 53 | } 54 | 55 | handleDrop(e) { 56 | e.preventDefault(); 57 | e.stopPropagation(); 58 | e.currentTarget.style.backgroundColor = ''; 59 | e.currentTarget.style.borderColor = ''; 60 | 61 | const files = e.dataTransfer.files; 62 | if (files.length > 0) { 63 | this.processFile(files[0]); 64 | } 65 | } 66 | 67 | handleFileSelect(e) { 68 | const files = e.target.files; 69 | if (files.length > 0) { 70 | this.processFile(files[0]); 71 | } 72 | } 73 | 74 | processFile(file) { 75 | if (!file.type.startsWith('image/')) { 76 | window.app?.showMessage('Please select a valid image file.', 'error'); 77 | return; 78 | } 79 | 80 | if (file.size > 50 * 1024 * 1024) { 81 | window.app?.showMessage('File size should be less than 50MB.', 'error'); 82 | return; 83 | } 84 | 85 | const reader = new FileReader(); 86 | reader.onload = (e) => { 87 | this.loadImage(e.target.result, file); 88 | }; 89 | reader.readAsDataURL(file); 90 | } 91 | 92 | loadImage(dataUrl, file) { 93 | const img = new Image(); 94 | img.onload = () => { 95 | this.currentImage = { 96 | img, 97 | dataUrl, 98 | file, 99 | width: img.width, 100 | height: img.height 101 | }; 102 | this.showImagePreview(); 103 | this.showConversionOptions(); 104 | }; 105 | img.src = dataUrl; 106 | } 107 | 108 | showImagePreview() { 109 | const preview = document.getElementById('image-preview'); 110 | const previewImg = document.getElementById('preview-image'); 111 | const imageName = document.getElementById('image-name'); 112 | const imageSize = document.getElementById('image-size'); 113 | const imageDimensions = document.getElementById('image-dimensions'); 114 | 115 | previewImg.src = this.currentImage.dataUrl; 116 | imageName.textContent = this.currentImage.file.name; 117 | imageSize.textContent = this.formatFileSize(this.currentImage.file.size); 118 | imageDimensions.textContent = `${this.currentImage.width} × ${this.currentImage.height}`; 119 | 120 | preview.style.display = 'block'; 121 | } 122 | 123 | showConversionOptions() { 124 | const options = document.getElementById('conversion-options'); 125 | options.style.display = 'block'; 126 | this.toggleQualityOptions(); 127 | } 128 | 129 | toggleQualityOptions() { 130 | const format = document.getElementById('output-format').value; 131 | const qualityOptions = document.getElementById('quality-options'); 132 | 133 | if (format === 'jpeg' || format === 'webp') { 134 | qualityOptions.style.display = 'block'; 135 | } else { 136 | qualityOptions.style.display = 'none'; 137 | } 138 | } 139 | 140 | convertImage() { 141 | if (!this.currentImage) { 142 | window.app?.showMessage('Please select an image first.', 'error'); 143 | return; 144 | } 145 | 146 | const format = document.getElementById('output-format').value; 147 | const quality = document.getElementById('quality-slider').value / 100; 148 | 149 | const canvas = document.createElement('canvas'); 150 | const ctx = canvas.getContext('2d'); 151 | 152 | canvas.width = this.currentImage.width; 153 | canvas.height = this.currentImage.height; 154 | 155 | if (format === 'jpeg') { 156 | ctx.fillStyle = 'white'; 157 | ctx.fillRect(0, 0, canvas.width, canvas.height); 158 | } 159 | 160 | ctx.drawImage(this.currentImage.img, 0, 0); 161 | 162 | let mimeType = 'image/' + format; 163 | if (format === 'jpeg') { 164 | mimeType = 'image/jpeg'; 165 | } 166 | 167 | const convertedDataUrl = canvas.toDataURL(mimeType, quality); 168 | this.downloadImage(convertedDataUrl, format); 169 | } 170 | 171 | downloadImage(dataUrl, format) { 172 | const link = document.createElement('a'); 173 | const originalName = this.currentImage.file.name.split('.')[0]; 174 | 175 | link.download = `${originalName}.${format}`; 176 | link.href = dataUrl; 177 | link.click(); 178 | 179 | window.app?.showMessage(`Image converted and downloaded as ${format.toUpperCase()}!`, 'success'); 180 | } 181 | 182 | formatFileSize(bytes) { 183 | if (bytes === 0) return '0 Bytes'; 184 | 185 | const k = 1024; 186 | const sizes = ['Bytes', 'KB', 'MB', 'GB']; 187 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 188 | 189 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 190 | } 191 | } 192 | 193 | window.ImageConverter = new ImageConverter(); 194 | 195 | const imageStyles = ` 196 | .file-drop-zone { 197 | border: 2px dashed #d0d0d0; 198 | border-radius: 8px; 199 | padding: 40px 20px; 200 | text-align: center; 201 | cursor: pointer; 202 | transition: all 0.3s ease; 203 | background-color: #fafafa; 204 | } 205 | 206 | .file-drop-zone:hover { 207 | border-color: #667eea; 208 | background-color: #f0f8ff; 209 | } 210 | 211 | .file-drop-zone i { 212 | font-size: 3rem; 213 | color: #86868b; 214 | margin-bottom: 15px; 215 | } 216 | 217 | .file-drop-zone p { 218 | color: #86868b; 219 | margin: 0; 220 | font-size: 16px; 221 | } 222 | 223 | .image-preview { 224 | margin: 20px 0; 225 | text-align: center; 226 | } 227 | 228 | #preview-image { 229 | max-width: 100%; 230 | max-height: 300px; 231 | border-radius: 8px; 232 | box-shadow: 0 4px 20px rgba(0,0,0,0.1); 233 | } 234 | 235 | .image-info { 236 | display: flex; 237 | justify-content: center; 238 | gap: 20px; 239 | margin-top: 15px; 240 | flex-wrap: wrap; 241 | } 242 | 243 | .image-info span { 244 | background: #f0f0f0; 245 | padding: 5px 12px; 246 | border-radius: 20px; 247 | font-size: 12px; 248 | color: #666; 249 | } 250 | 251 | .conversion-options { 252 | background: #f8f9fa; 253 | border-radius: 8px; 254 | padding: 20px; 255 | margin-top: 20px; 256 | } 257 | 258 | .format-options, 259 | .quality-options { 260 | display: flex; 261 | align-items: center; 262 | gap: 15px; 263 | margin-bottom: 15px; 264 | flex-wrap: wrap; 265 | } 266 | 267 | .format-options label, 268 | .quality-options label { 269 | font-weight: 500; 270 | color: #1d1d1f; 271 | } 272 | 273 | .format-options select { 274 | padding: 8px 12px; 275 | border: 2px solid #f0f0f0; 276 | border-radius: 6px; 277 | font-size: 14px; 278 | } 279 | 280 | #quality-slider { 281 | flex: 1; 282 | min-width: 150px; 283 | } 284 | 285 | #quality-value { 286 | min-width: 35px; 287 | text-align: right; 288 | font-weight: bold; 289 | color: #667eea; 290 | } 291 | `; 292 | 293 | const imageConverterStyle = document.createElement('style'); 294 | style.textContent = imageStyles; 295 | document.head.appendChild(imageConverterStyle); -------------------------------------------------------------------------------- /src/renderer/js/tools/csv-json.js: -------------------------------------------------------------------------------- 1 | class CSVJSONConverter { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const tabButtons = document.querySelectorAll('#csv-json .tab-btn'); 8 | const csvToJsonBtn = document.getElementById('csv-to-json-btn'); 9 | const jsonToCsvBtn = document.getElementById('json-to-csv-btn'); 10 | 11 | tabButtons.forEach(button => { 12 | button.addEventListener('click', () => { 13 | this.switchTab(button.dataset.tab); 14 | }); 15 | }); 16 | 17 | if (csvToJsonBtn) { 18 | csvToJsonBtn.addEventListener('click', () => this.convertCSVToJSON()); 19 | } 20 | 21 | if (jsonToCsvBtn) { 22 | jsonToCsvBtn.addEventListener('click', () => this.convertJSONToCSV()); 23 | } 24 | 25 | const inputs = document.querySelectorAll('#csv-input, #json-for-csv-input'); 26 | inputs.forEach(input => { 27 | input.addEventListener('input', () => this.clearOutputs()); 28 | }); 29 | } 30 | 31 | switchTab(tabName) { 32 | const container = document.getElementById('csv-json'); 33 | 34 | container.querySelectorAll('.tab-btn').forEach(btn => { 35 | btn.classList.remove('active'); 36 | }); 37 | container.querySelectorAll('.tab-content').forEach(content => { 38 | content.classList.remove('active'); 39 | }); 40 | 41 | container.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); 42 | container.getElementById(`${tabName}-tab`).classList.add('active'); 43 | 44 | this.clearOutputs(); 45 | } 46 | 47 | convertCSVToJSON() { 48 | const input = document.getElementById('csv-input').value.trim(); 49 | const output = document.getElementById('json-from-csv-output'); 50 | const hasHeaders = document.getElementById('has-headers').checked; 51 | const delimiter = document.getElementById('csv-delimiter').value; 52 | 53 | if (!input) { 54 | window.app?.showMessage('Please enter CSV data to convert.', 'error'); 55 | return; 56 | } 57 | 58 | try { 59 | const jsonResult = this.csvToJson(input, delimiter, hasHeaders); 60 | output.textContent = JSON.stringify(jsonResult, null, 2); 61 | window.app?.showMessage('CSV converted to JSON successfully!', 'success'); 62 | } catch (error) { 63 | window.app?.showMessage('Error converting CSV: ' + error.message, 'error'); 64 | output.textContent = ''; 65 | } 66 | } 67 | 68 | convertJSONToCSV() { 69 | const input = document.getElementById('json-for-csv-input').value.trim(); 70 | const output = document.getElementById('csv-from-json-output'); 71 | 72 | if (!input) { 73 | window.app?.showMessage('Please enter JSON array to convert.', 'error'); 74 | return; 75 | } 76 | 77 | try { 78 | const jsonData = JSON.parse(input); 79 | if (!Array.isArray(jsonData)) { 80 | throw new Error('JSON must be an array of objects'); 81 | } 82 | 83 | const csvResult = this.jsonToCsv(jsonData); 84 | output.value = csvResult; 85 | window.app?.showMessage('JSON converted to CSV successfully!', 'success'); 86 | } catch (error) { 87 | window.app?.showMessage('Error converting JSON: ' + error.message, 'error'); 88 | output.value = ''; 89 | } 90 | } 91 | 92 | csvToJson(csvString, delimiter = ',', hasHeaders = true) { 93 | const lines = this.parseCSVLines(csvString); 94 | if (lines.length === 0) { 95 | throw new Error('No data found'); 96 | } 97 | 98 | const result = []; 99 | let headers = []; 100 | 101 | if (hasHeaders) { 102 | headers = this.parseCSVLine(lines[0], delimiter); 103 | for (let i = 1; i < lines.length; i++) { 104 | const values = this.parseCSVLine(lines[i], delimiter); 105 | if (values.length > 0) { 106 | const obj = {}; 107 | headers.forEach((header, index) => { 108 | obj[header] = values[index] || ''; 109 | }); 110 | result.push(obj); 111 | } 112 | } 113 | } else { 114 | for (let i = 0; i < lines.length; i++) { 115 | const values = this.parseCSVLine(lines[i], delimiter); 116 | if (values.length > 0) { 117 | result.push(values); 118 | } 119 | } 120 | } 121 | 122 | return result; 123 | } 124 | 125 | jsonToCsv(jsonArray) { 126 | if (jsonArray.length === 0) { 127 | return ''; 128 | } 129 | 130 | const headers = Object.keys(jsonArray[0]); 131 | const csvLines = []; 132 | 133 | csvLines.push(headers.map(header => this.escapeCSVValue(header)).join(',')); 134 | 135 | jsonArray.forEach(obj => { 136 | const values = headers.map(header => { 137 | const value = obj[header]; 138 | return this.escapeCSVValue(value !== undefined ? String(value) : ''); 139 | }); 140 | csvLines.push(values.join(',')); 141 | }); 142 | 143 | return csvLines.join('\n'); 144 | } 145 | 146 | parseCSVLines(csvString) { 147 | return csvString.split(/\r?\n/).filter(line => line.trim()); 148 | } 149 | 150 | parseCSVLine(line, delimiter = ',') { 151 | const result = []; 152 | let current = ''; 153 | let inQuotes = false; 154 | let quoteChar = '"'; 155 | 156 | for (let i = 0; i < line.length; i++) { 157 | const char = line[i]; 158 | const nextChar = line[i + 1]; 159 | 160 | if (char === quoteChar && !inQuotes) { 161 | inQuotes = true; 162 | } else if (char === quoteChar && inQuotes) { 163 | if (nextChar === quoteChar) { 164 | current += quoteChar; 165 | i++; 166 | } else { 167 | inQuotes = false; 168 | } 169 | } else if (char === delimiter && !inQuotes) { 170 | result.push(current.trim()); 171 | current = ''; 172 | } else { 173 | current += char; 174 | } 175 | } 176 | 177 | result.push(current.trim()); 178 | return result; 179 | } 180 | 181 | escapeCSVValue(value) { 182 | if (value.includes(',') || value.includes('"') || value.includes('\n')) { 183 | return '"' + value.replace(/"/g, '""') + '"'; 184 | } 185 | return value; 186 | } 187 | 188 | clearOutputs() { 189 | document.getElementById('json-from-csv-output').textContent = ''; 190 | document.getElementById('csv-from-json-output').value = ''; 191 | } 192 | 193 | downloadCSV() { 194 | const output = document.getElementById('csv-from-json-output'); 195 | if (output.value.trim()) { 196 | const blob = new Blob([output.value], { type: 'text/csv' }); 197 | const url = URL.createObjectURL(blob); 198 | const a = document.createElement('a'); 199 | a.href = url; 200 | a.download = 'converted.csv'; 201 | a.click(); 202 | URL.revokeObjectURL(url); 203 | } 204 | } 205 | 206 | downloadJSON() { 207 | const output = document.getElementById('json-from-csv-output'); 208 | if (output.textContent.trim()) { 209 | const blob = new Blob([output.textContent], { type: 'application/json' }); 210 | const url = URL.createObjectURL(blob); 211 | const a = document.createElement('a'); 212 | a.href = url; 213 | a.download = 'converted.json'; 214 | a.click(); 215 | URL.revokeObjectURL(url); 216 | } 217 | } 218 | } 219 | 220 | window.CSVJSONConverter = new CSVJSONConverter(); 221 | 222 | const csvJsonStyles = ` 223 | .csv-options { 224 | display: flex; 225 | gap: 20px; 226 | margin-bottom: 15px; 227 | flex-wrap: wrap; 228 | align-items: center; 229 | } 230 | 231 | .csv-options label { 232 | display: flex; 233 | align-items: center; 234 | gap: 8px; 235 | font-size: 14px; 236 | color: #1d1d1f; 237 | white-space: nowrap; 238 | } 239 | 240 | .csv-options select { 241 | padding: 4px 8px; 242 | border: 1px solid #d0d0d0; 243 | border-radius: 4px; 244 | font-size: 14px; 245 | } 246 | 247 | .csv-options input[type="checkbox"] { 248 | margin: 0; 249 | } 250 | 251 | #csv-input, 252 | #json-for-csv-input { 253 | min-height: 150px; 254 | font-family: 'Monaco', 'Consolas', monospace; 255 | font-size: 13px; 256 | } 257 | 258 | #json-from-csv-output { 259 | background-color: #f8f9fa; 260 | border: 2px solid #f0f0f0; 261 | border-radius: 8px; 262 | padding: 15px; 263 | font-family: 'Monaco', 'Consolas', monospace; 264 | font-size: 12px; 265 | line-height: 1.4; 266 | white-space: pre; 267 | overflow: auto; 268 | max-height: 400px; 269 | min-height: 150px; 270 | } 271 | 272 | #csv-from-json-output { 273 | min-height: 150px; 274 | font-family: 'Monaco', 'Consolas', monospace; 275 | font-size: 13px; 276 | } 277 | 278 | .converter-tabs { 279 | border-bottom: 2px solid #f0f0f0; 280 | margin-bottom: 20px; 281 | } 282 | 283 | .tab-content { 284 | display: none; 285 | } 286 | 287 | .tab-content.active { 288 | display: block; 289 | } 290 | 291 | .tab-content textarea, 292 | .tab-content pre { 293 | width: 100%; 294 | margin: 15px 0; 295 | } 296 | `; 297 | 298 | const csvJsonStyle = document.createElement('style'); 299 | csvJsonStyle.textContent = csvJsonStyles; 300 | document.head.appendChild(csvJsonStyle); -------------------------------------------------------------------------------- /src/utils/api-formatter-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility class for API response formatting with tree view functionality 3 | */ 4 | class APIFormatterUtils { 5 | static parseResponse(input, responseType = 'json') { 6 | if (!input || !input.trim()) { 7 | throw new Error('Input cannot be empty'); 8 | } 9 | 10 | const trimmedInput = input.trim(); 11 | 12 | try { 13 | switch (responseType.toLowerCase()) { 14 | case 'json': 15 | return JSON.parse(trimmedInput); 16 | case 'graphql': 17 | // GraphQL responses are JSON format 18 | const parsed = JSON.parse(trimmedInput); 19 | this.validateGraphQLResponse(parsed); 20 | return parsed; 21 | default: 22 | throw new Error('Unsupported response type'); 23 | } 24 | } catch (error) { 25 | if ( 26 | error.message.includes('Unexpected token') || 27 | error.message.includes('Expected') 28 | ) { 29 | throw new Error( 30 | `Invalid ${responseType.toUpperCase()} format: ${error.message}` 31 | ); 32 | } 33 | throw error; 34 | } 35 | } 36 | 37 | static validateGraphQLResponse(response) { 38 | // GraphQL response should have data and/or errors fields 39 | if (typeof response !== 'object' || response === null) { 40 | throw new Error('GraphQL response must be an object'); 41 | } 42 | 43 | if ( 44 | !response.hasOwnProperty('data') && 45 | !response.hasOwnProperty('errors') 46 | ) { 47 | throw new Error( 48 | 'GraphQL response must contain either "data" or "errors" field' 49 | ); 50 | } 51 | } 52 | 53 | static generateTreeHTML(data, path = '', level = 0) { 54 | if (data === null) { 55 | return `null`; 56 | } 57 | 58 | if (typeof data === 'undefined') { 59 | return `undefined`; 60 | } 61 | 62 | if (typeof data === 'string') { 63 | const escaped = this.escapeHtml(data); 64 | return `"${escaped}"`; 65 | } 66 | 67 | if (typeof data === 'number') { 68 | return `${data}`; 69 | } 70 | 71 | if (typeof data === 'boolean') { 72 | return `${data}`; 73 | } 74 | 75 | if (Array.isArray(data)) { 76 | return this.generateArrayHTML(data, path, level); 77 | } 78 | 79 | if (typeof data === 'object') { 80 | return this.generateObjectHTML(data, path, level); 81 | } 82 | 83 | return `${String(data)}`; 84 | } 85 | 86 | static generateArrayHTML(array, path, level) { 87 | if (array.length === 0) { 88 | return `[]`; 89 | } 90 | 91 | const itemId = `array_${path.replace( 92 | /[^a-zA-Z0-9]/g, 93 | '_' 94 | )}_${level}_${Date.now()}`; 95 | const isCollapsible = array.length > 0; 96 | 97 | let html = `
`; 98 | 99 | if (isCollapsible) { 100 | html += ` 101 | 102 | 103 | 104 | `; 105 | } 106 | 107 | html += `[`; 108 | html += `${array.length} item${ 109 | array.length !== 1 ? 's' : '' 110 | }`; 111 | 112 | if (isCollapsible) { 113 | html += `
`; 114 | 115 | array.forEach((item, index) => { 116 | const itemPath = `${path}[${index}]`; 117 | html += ` 118 |
119 | ${index}: 120 | ${this.generateTreeHTML(item, itemPath, level + 1)} 121 |
122 | `; 123 | }); 124 | 125 | html += `
`; 126 | } 127 | 128 | html += `]`; 129 | html += `
`; 130 | 131 | return html; 132 | } 133 | 134 | static generateObjectHTML(obj, path, level) { 135 | const keys = Object.keys(obj); 136 | 137 | if (keys.length === 0) { 138 | return `{}`; 139 | } 140 | 141 | const itemId = `object_${path.replace( 142 | /[^a-zA-Z0-9]/g, 143 | '_' 144 | )}_${level}_${Date.now()}`; 145 | const isCollapsible = keys.length > 0; 146 | 147 | let html = `
`; 148 | 149 | if (isCollapsible) { 150 | html += ` 151 | 152 | 153 | 154 | `; 155 | } 156 | 157 | html += `{`; 158 | html += `${keys.length} key${ 159 | keys.length !== 1 ? 's' : '' 160 | }`; 161 | 162 | if (isCollapsible) { 163 | html += `
`; 164 | 165 | keys.forEach((key, index) => { 166 | const keyPath = path ? `${path}.${key}` : key; 167 | const isLast = index === keys.length - 1; 168 | 169 | html += ` 170 |
171 | "${this.escapeHtml(key)}" 172 | : 173 | ${this.generateTreeHTML(obj[key], keyPath, level + 1)} 174 | ${!isLast ? ',' : ''} 175 |
176 | `; 177 | }); 178 | 179 | html += `
`; 180 | } 181 | 182 | html += `}`; 183 | html += `
`; 184 | 185 | return html; 186 | } 187 | 188 | static escapeHtml(text) { 189 | try { 190 | if (typeof document !== 'undefined' && document.createElement) { 191 | const div = document.createElement('div'); 192 | if (div && typeof div.textContent !== 'undefined') { 193 | div.textContent = text; 194 | return div.innerHTML; 195 | } 196 | } 197 | } catch (e) { 198 | // Fall through to manual escaping 199 | } 200 | 201 | // Fallback for testing environments without DOM or when DOM fails 202 | return text 203 | .replace(/&/g, '&') 204 | .replace(//g, '>') 206 | .replace(/"/g, '"') 207 | .replace(/'/g, '''); 208 | } 209 | 210 | static getResponseInfo(data) { 211 | const jsonString = JSON.stringify(data); 212 | return { 213 | type: Array.isArray(data) ? 'Array' : typeof data, 214 | size: this.formatBytes(new Blob([jsonString]).size), 215 | keys: this.countKeys(data), 216 | depth: this.getMaxDepth(data), 217 | charactersCount: jsonString.length, 218 | }; 219 | } 220 | 221 | static countKeys(obj) { 222 | if (typeof obj !== 'object' || obj === null) { 223 | return 0; 224 | } 225 | 226 | if (Array.isArray(obj)) { 227 | return obj.length; 228 | } 229 | 230 | let count = 0; 231 | for (let key in obj) { 232 | if (obj.hasOwnProperty(key)) { 233 | count++; 234 | if (typeof obj[key] === 'object' && obj[key] !== null) { 235 | count += this.countKeys(obj[key]); 236 | } 237 | } 238 | } 239 | return count; 240 | } 241 | 242 | static getMaxDepth(obj, depth = 0) { 243 | if (typeof obj !== 'object' || obj === null) { 244 | return depth; 245 | } 246 | 247 | let maxDepth = depth; 248 | 249 | if (Array.isArray(obj)) { 250 | for (let item of obj) { 251 | maxDepth = Math.max(maxDepth, this.getMaxDepth(item, depth + 1)); 252 | } 253 | } else { 254 | for (let key in obj) { 255 | if (obj.hasOwnProperty(key)) { 256 | maxDepth = Math.max(maxDepth, this.getMaxDepth(obj[key], depth + 1)); 257 | } 258 | } 259 | } 260 | 261 | return maxDepth; 262 | } 263 | 264 | static formatBytes(bytes) { 265 | if (bytes === 0) return '0 B'; 266 | 267 | const k = 1024; 268 | const sizes = ['B', 'KB', 'MB', 'GB']; 269 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 270 | 271 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 272 | } 273 | 274 | static searchInData(data, searchTerm, path = '') { 275 | const results = []; 276 | 277 | if (!searchTerm || !searchTerm.trim()) { 278 | return results; 279 | } 280 | 281 | const term = searchTerm.toLowerCase(); 282 | 283 | if (typeof data === 'string' && data.toLowerCase().includes(term)) { 284 | results.push({ 285 | path: path || 'root', 286 | type: 'value', 287 | value: data, 288 | match: 'content', 289 | }); 290 | } 291 | 292 | if (typeof data === 'object' && data !== null) { 293 | if (Array.isArray(data)) { 294 | data.forEach((item, index) => { 295 | const itemPath = path ? `${path}[${index}]` : `[${index}]`; 296 | results.push(...this.searchInData(item, searchTerm, itemPath)); 297 | }); 298 | } else { 299 | for (let key in data) { 300 | if (data.hasOwnProperty(key)) { 301 | const keyPath = path ? `${path}.${key}` : key; 302 | 303 | // Check if key matches 304 | if (key.toLowerCase().includes(term)) { 305 | results.push({ 306 | path: keyPath, 307 | type: 'key', 308 | value: key, 309 | match: 'key', 310 | }); 311 | } 312 | 313 | // Search in value 314 | results.push(...this.searchInData(data[key], searchTerm, keyPath)); 315 | } 316 | } 317 | } 318 | } 319 | 320 | return results; 321 | } 322 | 323 | static highlightSearchResults(html, searchTerm) { 324 | if (!searchTerm || !searchTerm.trim()) { 325 | return html; 326 | } 327 | 328 | // Escape regex special characters and HTML entities 329 | const term = this.escapeHtml(searchTerm).replace( 330 | /[.*+?^${}()|[\]\\]/g, 331 | '\\$&' 332 | ); 333 | const regex = new RegExp(`(${term})`, 'gi'); 334 | 335 | return html.replace(regex, '$1'); 336 | } 337 | 338 | static expandToPath(path) { 339 | // Generate selectors to expand all nodes in the path 340 | const pathParts = path.split(/[\.\[\]]/g).filter((part) => part !== ''); 341 | const selectors = []; 342 | 343 | let currentPath = ''; 344 | pathParts.forEach((part, index) => { 345 | if (index === 0) { 346 | currentPath = part; 347 | } else { 348 | currentPath += pathParts[index - 1].match(/^\d+$/) 349 | ? `[${part}]` 350 | : `.${part}`; 351 | } 352 | 353 | const elementId = `object_${currentPath.replace( 354 | /[^a-zA-Z0-9]/g, 355 | '_' 356 | )}_${index}`; 357 | selectors.push(`#${elementId}`); 358 | }); 359 | 360 | return selectors; 361 | } 362 | } 363 | 364 | // Export for testing 365 | if (typeof module !== 'undefined' && module.exports) { 366 | module.exports = APIFormatterUtils; 367 | } 368 | 369 | // Export for browser 370 | if (typeof window !== 'undefined') { 371 | window.APIFormatterUtils = APIFormatterUtils; 372 | } 373 | -------------------------------------------------------------------------------- /src/renderer/js/tools/lorem-generator.js: -------------------------------------------------------------------------------- 1 | class LoremGenerator { 2 | constructor() { 3 | // Use the same word list as our tested utils 4 | this.LOREM_WORDS = [ 5 | 'lorem', 6 | 'ipsum', 7 | 'dolor', 8 | 'sit', 9 | 'amet', 10 | 'consectetur', 11 | 'adipiscing', 12 | 'elit', 13 | 'sed', 14 | 'do', 15 | 'eiusmod', 16 | 'tempor', 17 | 'incididunt', 18 | 'ut', 19 | 'labore', 20 | 'et', 21 | 'dolore', 22 | 'magna', 23 | 'aliqua', 24 | 'enim', 25 | 'ad', 26 | 'minim', 27 | 'veniam', 28 | 'quis', 29 | 'nostrud', 30 | 'exercitation', 31 | 'ullamco', 32 | 'laboris', 33 | 'nisi', 34 | 'aliquip', 35 | 'ex', 36 | 'ea', 37 | 'commodo', 38 | 'consequat', 39 | 'duis', 40 | 'aute', 41 | 'irure', 42 | 'in', 43 | 'reprehenderit', 44 | 'voluptate', 45 | 'velit', 46 | 'esse', 47 | 'cillum', 48 | 'fugiat', 49 | 'nulla', 50 | 'pariatur', 51 | 'excepteur', 52 | 'sint', 53 | 'occaecat', 54 | 'cupidatat', 55 | 'non', 56 | 'proident', 57 | 'sunt', 58 | 'culpa', 59 | 'qui', 60 | 'officia', 61 | 'deserunt', 62 | 'mollit', 63 | 'anim', 64 | 'id', 65 | 'est', 66 | 'laborum', 67 | 'at', 68 | 'vero', 69 | 'eos', 70 | 'accusamus', 71 | 'accusantium', 72 | 'doloremque', 73 | 'laudantium', 74 | 'totam', 75 | 'rem', 76 | 'aperiam', 77 | 'eaque', 78 | 'ipsa', 79 | 'quae', 80 | 'ab', 81 | 'illo', 82 | 'inventore', 83 | 'veritatis', 84 | 'et', 85 | 'quasi', 86 | 'architecto', 87 | 'beatae', 88 | 'vitae', 89 | 'dicta', 90 | 'sunt', 91 | 'explicabo', 92 | 'nemo', 93 | 'ipsam', 94 | 'voluptatem', 95 | 'quia', 96 | 'voluptas', 97 | 'aspernatur', 98 | 'aut', 99 | 'odit', 100 | 'fugit', 101 | 'sed', 102 | 'quia', 103 | 'consequuntur', 104 | 'magni', 105 | 'dolores', 106 | 'ratione', 107 | 'sequi', 108 | 'nesciunt', 109 | 'neque', 110 | 'porro', 111 | 'quisquam', 112 | 'dolorem', 113 | 'adipisci', 114 | 'numquam', 115 | 'eius', 116 | 'modi', 117 | 'tempora', 118 | 'incidunt', 119 | 'magnam', 120 | 'quaerat', 121 | 'voluptatem', 122 | 'laudantium', 123 | 'doloremque', 124 | 'laudantium', 125 | ]; 126 | this.init(); 127 | } 128 | 129 | init() { 130 | const generateBtn = document.getElementById('generate-lorem'); 131 | const typeSelect = document.getElementById('lorem-type'); 132 | const countInput = document.getElementById('lorem-count'); 133 | 134 | console.log('LoremGenerator init:', { 135 | generateBtn: !!generateBtn, 136 | typeSelect: !!typeSelect, 137 | countInput: !!countInput, 138 | }); 139 | 140 | // Alert for debugging in case console isn't visible 141 | if (!generateBtn || !typeSelect || !countInput) { 142 | window.app?.showMessage( 143 | 'Lorem Generator: Some elements not found!', 144 | 'error' 145 | ); 146 | } 147 | 148 | if (generateBtn) { 149 | generateBtn.addEventListener('click', (e) => { 150 | console.log('Generate lorem button clicked'); 151 | window.app?.showMessage('Button clicked!', 'info'); 152 | this.generateLorem(); 153 | }); 154 | } else { 155 | console.error('Generate button not found!'); 156 | window.app?.showMessage('Generate button not found!', 'error'); 157 | } 158 | 159 | if (typeSelect) { 160 | typeSelect.addEventListener('change', () => this.clearOutput()); 161 | } 162 | 163 | if (countInput) { 164 | countInput.addEventListener('input', () => this.clearOutput()); 165 | } 166 | } 167 | 168 | generateLorem() { 169 | const typeElement = document.getElementById('lorem-type'); 170 | const countElement = document.getElementById('lorem-count'); 171 | const output = document.getElementById('lorem-output'); 172 | 173 | if (!typeElement || !countElement || !output) { 174 | console.error('Lorem generator: Required elements not found', { 175 | typeElement: !!typeElement, 176 | countElement: !!countElement, 177 | output: !!output, 178 | }); 179 | return; 180 | } 181 | 182 | const type = typeElement.value; 183 | const count = parseInt(countElement.value); 184 | 185 | if (isNaN(count) || count < 1 || count > 50) { 186 | window.app?.showMessage( 187 | 'Please enter a count between 1 and 50.', 188 | 'error' 189 | ); 190 | return; 191 | } 192 | 193 | try { 194 | let result = ''; 195 | 196 | switch (type) { 197 | case 'paragraphs': 198 | result = this.generateParagraphs(count); 199 | break; 200 | case 'sentences': 201 | result = this.generateSentences(count); 202 | break; 203 | case 'words': 204 | result = this.generateWords(count); 205 | break; 206 | default: 207 | result = this.generateParagraphs(count); 208 | } 209 | 210 | output.value = result; 211 | window.app?.showMessage(`Generated ${count} ${type}!`, 'success'); 212 | this.selectOutput(); 213 | } catch (error) { 214 | console.error('Error generating lorem text:', error); 215 | window.app?.showMessage( 216 | 'Error generating lorem text: ' + error.message, 217 | 'error' 218 | ); 219 | } 220 | } 221 | 222 | generateParagraphs(count) { 223 | if (count < 1) { 224 | throw new Error('Paragraph count must be at least 1'); 225 | } 226 | 227 | const paragraphs = []; 228 | for (let i = 0; i < count; i++) { 229 | const sentences = this.generateSentences(this.randomBetween(3, 7)); 230 | paragraphs.push(sentences); 231 | } 232 | return paragraphs.join('\n\n'); 233 | } 234 | 235 | generateSentences(count) { 236 | if (count < 1) { 237 | throw new Error('Sentence count must be at least 1'); 238 | } 239 | 240 | const sentences = []; 241 | for (let i = 0; i < count; i++) { 242 | const wordCount = this.randomBetween(8, 20); 243 | const words = this.generateWords(wordCount).split(' '); 244 | 245 | words[0] = this.capitalizeFirst(words[0]); 246 | 247 | let sentence = words.join(' '); 248 | 249 | if (Math.random() < 0.1) { 250 | sentence += '!'; 251 | } else if (Math.random() < 0.1) { 252 | sentence += '?'; 253 | } else { 254 | sentence += '.'; 255 | } 256 | 257 | sentences.push(sentence); 258 | } 259 | return sentences.join(' '); 260 | } 261 | 262 | generateWords(count) { 263 | if (count < 1) { 264 | throw new Error('Word count must be at least 1'); 265 | } 266 | 267 | const words = []; 268 | for (let i = 0; i < count; i++) { 269 | const word = 270 | this.LOREM_WORDS[Math.floor(Math.random() * this.LOREM_WORDS.length)]; 271 | words.push(word); 272 | } 273 | return words.join(' '); 274 | } 275 | 276 | randomBetween(min, max) { 277 | return Math.floor(Math.random() * (max - min + 1)) + min; 278 | } 279 | 280 | capitalizeFirst(str) { 281 | return str.charAt(0).toUpperCase() + str.slice(1); 282 | } 283 | 284 | selectOutput() { 285 | const output = document.getElementById('lorem-output'); 286 | setTimeout(() => { 287 | output.select(); 288 | output.setSelectionRange(0, 99999); 289 | }, 100); 290 | } 291 | 292 | clearOutput() { 293 | document.getElementById('lorem-output').value = ''; 294 | } 295 | 296 | copyToClipboard() { 297 | const output = document.getElementById('lorem-output'); 298 | if (output.value.trim()) { 299 | navigator.clipboard.writeText(output.value).then(() => { 300 | window.app?.showMessage('Lorem text copied to clipboard!', 'success'); 301 | }); 302 | } 303 | } 304 | 305 | // Test method that can be called directly 306 | testGeneration() { 307 | window.app?.showMessage('Testing lorem generation...', 'info'); 308 | try { 309 | const words = this.generateWords(5); 310 | window.app?.showMessage(`Generated words: ${words}`, 'success'); 311 | return words; 312 | } catch (error) { 313 | window.app?.showMessage(`Error: ${error.message}`, 'error'); 314 | return null; 315 | } 316 | } 317 | 318 | // Direct method to test DOM elements 319 | testDOMElements() { 320 | const generateBtn = document.getElementById('generate-lorem'); 321 | const typeSelect = document.getElementById('lorem-type'); 322 | const countInput = document.getElementById('lorem-count'); 323 | const output = document.getElementById('lorem-output'); 324 | 325 | const results = { 326 | generateBtn: !!generateBtn, 327 | typeSelect: !!typeSelect, 328 | countInput: !!countInput, 329 | output: !!output, 330 | generateBtnVisible: generateBtn 331 | ? window.getComputedStyle(generateBtn).display !== 'none' 332 | : false, 333 | containerActive: !!document.querySelector('#lorem-generator.active'), 334 | }; 335 | 336 | console.log('DOM Elements Test:', results); 337 | window.app?.showMessage(`DOM test: ${JSON.stringify(results)}`, 'info'); 338 | return results; 339 | } 340 | } 341 | 342 | window.LoremGenerator = new LoremGenerator(); 343 | 344 | // Also ensure initialization happens when DOM is fully loaded 345 | document.addEventListener('DOMContentLoaded', () => { 346 | if (window.LoremGenerator) { 347 | console.log('DOM loaded, reinitializing LoremGenerator'); 348 | window.LoremGenerator.init(); 349 | } 350 | }); 351 | 352 | // Add event delegation as backup 353 | document.addEventListener('click', (e) => { 354 | if (e.target && e.target.id === 'generate-lorem') { 355 | console.log('Generate lorem clicked via delegation'); 356 | window.app?.showMessage('Clicked via delegation!', 'info'); 357 | if (window.LoremGenerator) { 358 | window.LoremGenerator.generateLorem(); 359 | } 360 | } 361 | }); 362 | 363 | // Global test functions for debugging 364 | window.testLoremGeneration = function () { 365 | if (window.LoremGenerator) { 366 | return window.LoremGenerator.testGeneration(); 367 | } 368 | return 'LoremGenerator not found'; 369 | }; 370 | 371 | window.testLoremDOM = function () { 372 | if (window.LoremGenerator) { 373 | return window.LoremGenerator.testDOMElements(); 374 | } 375 | return 'LoremGenerator not found'; 376 | }; 377 | 378 | window.forceLoremGeneration = function () { 379 | if (window.LoremGenerator) { 380 | window.app?.showMessage('Force generating lorem...', 'info'); 381 | window.LoremGenerator.generateLorem(); 382 | return 'Forced generation attempted'; 383 | } 384 | return 'LoremGenerator not found'; 385 | }; 386 | 387 | const loremStyles = ` 388 | .lorem-controls { 389 | display: flex; 390 | align-items: end; 391 | gap: 20px; 392 | margin-bottom: 20px; 393 | flex-wrap: wrap; 394 | } 395 | 396 | .lorem-controls .control-group { 397 | display: flex; 398 | flex-direction: column; 399 | gap: 5px; 400 | } 401 | 402 | .lorem-controls label { 403 | font-weight: 500; 404 | color: #1d1d1f; 405 | } 406 | 407 | .lorem-controls select, 408 | .lorem-controls input { 409 | padding: 8px 12px; 410 | border: 2px solid #f0f0f0; 411 | border-radius: 6px; 412 | font-size: 14px; 413 | transition: border-color 0.3s ease; 414 | } 415 | 416 | .lorem-controls select:focus, 417 | .lorem-controls input:focus { 418 | outline: none; 419 | border-color: #667eea; 420 | } 421 | 422 | #lorem-output { 423 | min-height: 300px; 424 | line-height: 1.6; 425 | } 426 | `; 427 | 428 | const loremStyleElement = document.createElement('style'); 429 | loremStyleElement.textContent = loremStyles; 430 | document.head.appendChild(loremStyleElement); 431 | -------------------------------------------------------------------------------- /src/renderer/js/tools/qr-generator.js: -------------------------------------------------------------------------------- 1 | class QRGenerator { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const generateBtn = document.getElementById('generate-qr'); 8 | const downloadBtn = document.getElementById('download-qr'); 9 | const textInput = document.getElementById('qr-text'); 10 | 11 | if (generateBtn) { 12 | generateBtn.addEventListener('click', () => this.generateQR()); 13 | } 14 | 15 | if (downloadBtn) { 16 | downloadBtn.addEventListener('click', () => this.downloadQR()); 17 | } 18 | 19 | if (textInput) { 20 | textInput.addEventListener('input', () => this.onInputChange()); 21 | } 22 | } 23 | 24 | generateQR() { 25 | const text = document.getElementById('qr-text').value.trim(); 26 | const size = parseInt(document.getElementById('qr-size').value); 27 | const errorLevel = document.getElementById('error-level').value; 28 | 29 | if (!text) { 30 | window.app?.showMessage('Please enter text to generate QR code.', 'error'); 31 | return; 32 | } 33 | 34 | try { 35 | this.createQRCode(text, size, errorLevel); 36 | document.getElementById('download-qr').disabled = false; 37 | window.app?.showMessage('QR code generated successfully!', 'success'); 38 | } catch (error) { 39 | window.app?.showMessage('Error generating QR code: ' + error.message, 'error'); 40 | } 41 | } 42 | 43 | createQRCode(text, size, errorLevel) { 44 | const canvas = document.getElementById('qr-canvas'); 45 | const ctx = canvas.getContext('2d'); 46 | 47 | canvas.width = size; 48 | canvas.height = size; 49 | 50 | ctx.fillStyle = 'white'; 51 | ctx.fillRect(0, 0, size, size); 52 | 53 | const qr = this.generateQRMatrix(text, errorLevel); 54 | const moduleSize = Math.floor(size / qr.length); 55 | const offset = (size - (qr.length * moduleSize)) / 2; 56 | 57 | ctx.fillStyle = 'black'; 58 | for (let row = 0; row < qr.length; row++) { 59 | for (let col = 0; col < qr[row].length; col++) { 60 | if (qr[row][col]) { 61 | ctx.fillRect( 62 | offset + col * moduleSize, 63 | offset + row * moduleSize, 64 | moduleSize, 65 | moduleSize 66 | ); 67 | } 68 | } 69 | } 70 | 71 | document.getElementById('qr-preview').style.display = 'block'; 72 | } 73 | 74 | generateQRMatrix(text, errorLevel) { 75 | const version = this.determineVersion(text); 76 | const size = 17 + 4 * version; 77 | const matrix = Array(size).fill().map(() => Array(size).fill(false)); 78 | 79 | this.addFinderPatterns(matrix, size); 80 | this.addSeparators(matrix, size); 81 | this.addDarkModule(matrix, version); 82 | this.addTimingPatterns(matrix, size); 83 | 84 | if (version >= 2) { 85 | this.addAlignmentPatterns(matrix, version); 86 | } 87 | 88 | if (version >= 7) { 89 | this.addVersionInfo(matrix, version); 90 | } 91 | 92 | const data = this.encodeData(text, version, errorLevel); 93 | this.addDataToMatrix(matrix, data); 94 | 95 | return matrix; 96 | } 97 | 98 | determineVersion(text) { 99 | const length = text.length; 100 | if (length <= 17) return 1; 101 | if (length <= 32) return 2; 102 | if (length <= 53) return 3; 103 | if (length <= 78) return 4; 104 | return 5; 105 | } 106 | 107 | addFinderPatterns(matrix, size) { 108 | const pattern = [ 109 | [1,1,1,1,1,1,1], 110 | [1,0,0,0,0,0,1], 111 | [1,0,1,1,1,0,1], 112 | [1,0,1,1,1,0,1], 113 | [1,0,1,1,1,0,1], 114 | [1,0,0,0,0,0,1], 115 | [1,1,1,1,1,1,1] 116 | ]; 117 | 118 | const positions = [ 119 | [0, 0], 120 | [0, size - 7], 121 | [size - 7, 0] 122 | ]; 123 | 124 | positions.forEach(([startRow, startCol]) => { 125 | for (let row = 0; row < 7; row++) { 126 | for (let col = 0; col < 7; col++) { 127 | matrix[startRow + row][startCol + col] = pattern[row][col] === 1; 128 | } 129 | } 130 | }); 131 | } 132 | 133 | addSeparators(matrix, size) { 134 | for (let i = 0; i < 8; i++) { 135 | matrix[7][i] = false; 136 | matrix[i][7] = false; 137 | matrix[7][size - 8 + i] = false; 138 | matrix[i][size - 8] = false; 139 | matrix[size - 8][i] = false; 140 | matrix[size - 8 + i][7] = false; 141 | } 142 | } 143 | 144 | addDarkModule(matrix, version) { 145 | const size = matrix.length; 146 | matrix[4 * version + 9][8] = true; 147 | } 148 | 149 | addTimingPatterns(matrix, size) { 150 | for (let i = 8; i < size - 8; i++) { 151 | matrix[6][i] = (i % 2) === 0; 152 | matrix[i][6] = (i % 2) === 0; 153 | } 154 | } 155 | 156 | addAlignmentPatterns(matrix, version) { 157 | const positions = this.getAlignmentPatternPositions(version); 158 | 159 | positions.forEach(pos => { 160 | const [centerRow, centerCol] = pos; 161 | const pattern = [ 162 | [1,1,1,1,1], 163 | [1,0,0,0,1], 164 | [1,0,1,0,1], 165 | [1,0,0,0,1], 166 | [1,1,1,1,1] 167 | ]; 168 | 169 | for (let row = 0; row < 5; row++) { 170 | for (let col = 0; col < 5; col++) { 171 | const matrixRow = centerRow - 2 + row; 172 | const matrixCol = centerCol - 2 + col; 173 | if (matrixRow >= 0 && matrixRow < matrix.length && 174 | matrixCol >= 0 && matrixCol < matrix.length) { 175 | matrix[matrixRow][matrixCol] = pattern[row][col] === 1; 176 | } 177 | } 178 | } 179 | }); 180 | } 181 | 182 | getAlignmentPatternPositions(version) { 183 | const positions = { 184 | 2: [[6, 18]], 185 | 3: [[6, 22]], 186 | 4: [[6, 26]], 187 | 5: [[6, 30]] 188 | }; 189 | return positions[version] || []; 190 | } 191 | 192 | addVersionInfo() { 193 | } 194 | 195 | encodeData(text, version, errorLevel) { 196 | const mode = '0100'; 197 | const charCount = text.length.toString(2).padStart(8, '0'); 198 | 199 | let data = mode + charCount; 200 | 201 | for (let i = 0; i < text.length; i++) { 202 | data += text.charCodeAt(i).toString(2).padStart(8, '0'); 203 | } 204 | 205 | data += '0000'; 206 | 207 | const capacities = { 208 | 1: { L: 152, M: 128, Q: 104, H: 72 }, 209 | 2: { L: 272, M: 224, Q: 176, H: 128 }, 210 | 3: { L: 440, M: 352, Q: 272, H: 208 }, 211 | 4: { L: 640, M: 512, Q: 384, H: 288 }, 212 | 5: { L: 864, M: 688, Q: 496, H: 368 } 213 | }; 214 | 215 | const capacity = capacities[version][errorLevel]; 216 | 217 | while (data.length < capacity) { 218 | data += data.length % 16 < 8 ? '11101100' : '00010001'; 219 | } 220 | 221 | return data.substring(0, capacity); 222 | } 223 | 224 | addDataToMatrix(matrix, data) { 225 | let bitIndex = 0; 226 | let up = true; 227 | const size = matrix.length; 228 | 229 | for (let col = size - 1; col > 0; col -= 2) { 230 | if (col === 6) col--; 231 | 232 | for (let i = 0; i < size; i++) { 233 | for (let c = 0; c < 2; c++) { 234 | const currentCol = col - c; 235 | const currentRow = up ? size - 1 - i : i; 236 | 237 | if (!this.isReserved(matrix, currentRow, currentCol, size)) { 238 | if (bitIndex < data.length) { 239 | matrix[currentRow][currentCol] = data[bitIndex] === '1'; 240 | bitIndex++; 241 | } 242 | } 243 | } 244 | } 245 | up = !up; 246 | } 247 | } 248 | 249 | isReserved(_, row, col, size) { 250 | if (row < 9 && col < 9) return true; 251 | if (row < 9 && col >= size - 8) return true; 252 | if (row >= size - 8 && col < 9) return true; 253 | if (row === 6 || col === 6) return true; 254 | if (row === 4 * Math.floor((size - 17) / 4) + 9 && col === 8) return true; 255 | 256 | return false; 257 | } 258 | 259 | downloadQR() { 260 | const canvas = document.getElementById('qr-canvas'); 261 | if (!canvas.width) { 262 | window.app?.showMessage('Please generate a QR code first.', 'error'); 263 | return; 264 | } 265 | 266 | const link = document.createElement('a'); 267 | link.download = 'qrcode.png'; 268 | link.href = canvas.toDataURL(); 269 | link.click(); 270 | 271 | window.app?.showMessage('QR code downloaded!', 'success'); 272 | } 273 | 274 | onInputChange() { 275 | document.getElementById('download-qr').disabled = true; 276 | document.getElementById('qr-preview').style.display = 'none'; 277 | } 278 | 279 | generateQRFromUrl(url) { 280 | document.getElementById('qr-text').value = url; 281 | this.generateQR(); 282 | } 283 | } 284 | 285 | window.QRGenerator = new QRGenerator(); 286 | 287 | const qrStyles = ` 288 | #qr-text { 289 | width: 100%; 290 | min-height: 100px; 291 | padding: 15px; 292 | border: 2px solid #f0f0f0; 293 | border-radius: 8px; 294 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 295 | font-size: 14px; 296 | line-height: 1.5; 297 | resize: vertical; 298 | margin-bottom: 20px; 299 | } 300 | 301 | #qr-text:focus { 302 | outline: none; 303 | border-color: #667eea; 304 | } 305 | 306 | .qr-options { 307 | display: flex; 308 | gap: 30px; 309 | margin-bottom: 20px; 310 | flex-wrap: wrap; 311 | } 312 | 313 | .option-group { 314 | display: flex; 315 | flex-direction: column; 316 | gap: 8px; 317 | } 318 | 319 | .option-group label { 320 | font-weight: 500; 321 | color: #1d1d1f; 322 | font-size: 14px; 323 | } 324 | 325 | .option-group select { 326 | padding: 8px 12px; 327 | border: 2px solid #f0f0f0; 328 | border-radius: 6px; 329 | font-size: 14px; 330 | min-width: 150px; 331 | } 332 | 333 | .option-group select:focus { 334 | outline: none; 335 | border-color: #667eea; 336 | } 337 | 338 | .qr-preview { 339 | text-align: center; 340 | margin-top: 30px; 341 | padding: 20px; 342 | background: #f8f9fa; 343 | border-radius: 8px; 344 | border: 1px solid #e0e0e0; 345 | } 346 | 347 | #qr-canvas { 348 | border: 1px solid #d0d0d0; 349 | border-radius: 8px; 350 | background: white; 351 | max-width: 100%; 352 | height: auto; 353 | } 354 | 355 | .controls { 356 | display: flex; 357 | gap: 15px; 358 | margin-bottom: 20px; 359 | flex-wrap: wrap; 360 | } 361 | 362 | #download-qr:disabled { 363 | opacity: 0.5; 364 | cursor: not-allowed; 365 | } 366 | 367 | @media (max-width: 768px) { 368 | .qr-options { 369 | flex-direction: column; 370 | gap: 15px; 371 | } 372 | 373 | .option-group select { 374 | min-width: auto; 375 | width: 100%; 376 | } 377 | } 378 | `; 379 | 380 | const qrGeneratorStyle = document.createElement('style'); 381 | style.textContent = qrStyles; 382 | document.head.appendChild(qrGeneratorStyle); -------------------------------------------------------------------------------- /src/renderer/js/tools/cron-calculator.js: -------------------------------------------------------------------------------- 1 | class CronCalculator { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const generateBtn = document.getElementById('generate-cron'); 8 | const parseBtn = document.getElementById('parse-cron'); 9 | const cronFields = document.querySelectorAll('#cron-minute, #cron-hour, #cron-day, #cron-month, #cron-dow'); 10 | const expressionInput = document.getElementById('cron-expression'); 11 | 12 | if (generateBtn) { 13 | generateBtn.addEventListener('click', () => this.generateExpression()); 14 | } 15 | 16 | if (parseBtn) { 17 | parseBtn.addEventListener('click', () => this.parseExpression()); 18 | } 19 | 20 | cronFields.forEach(field => { 21 | field.addEventListener('input', () => this.updateFromFields()); 22 | }); 23 | 24 | if (expressionInput) { 25 | expressionInput.addEventListener('input', () => this.updateDescription()); 26 | } 27 | } 28 | 29 | generateExpression() { 30 | const minute = document.getElementById('cron-minute').value.trim() || '*'; 31 | const hour = document.getElementById('cron-hour').value.trim() || '*'; 32 | const day = document.getElementById('cron-day').value.trim() || '*'; 33 | const month = document.getElementById('cron-month').value.trim() || '*'; 34 | const dow = document.getElementById('cron-dow').value.trim() || '*'; 35 | 36 | const expression = `${minute} ${hour} ${day} ${month} ${dow}`; 37 | document.getElementById('cron-expression').value = expression; 38 | 39 | this.updateDescription(); 40 | this.calculateNextRuns(); 41 | window.app?.showMessage('Cron expression generated!', 'success'); 42 | } 43 | 44 | parseExpression() { 45 | const expression = document.getElementById('cron-expression').value.trim(); 46 | 47 | if (!expression) { 48 | window.app?.showMessage('Please enter a cron expression to parse.', 'error'); 49 | return; 50 | } 51 | 52 | const parts = expression.split(/\s+/); 53 | if (parts.length !== 5) { 54 | window.app?.showMessage('Invalid cron expression. Must have 5 fields.', 'error'); 55 | return; 56 | } 57 | 58 | document.getElementById('cron-minute').value = parts[0]; 59 | document.getElementById('cron-hour').value = parts[1]; 60 | document.getElementById('cron-day').value = parts[2]; 61 | document.getElementById('cron-month').value = parts[3]; 62 | document.getElementById('cron-dow').value = parts[4]; 63 | 64 | this.updateDescription(); 65 | this.calculateNextRuns(); 66 | window.app?.showMessage('Cron expression parsed!', 'success'); 67 | } 68 | 69 | updateFromFields() { 70 | this.generateExpression(); 71 | } 72 | 73 | updateDescription() { 74 | const expression = document.getElementById('cron-expression').value.trim(); 75 | const descriptionElement = document.getElementById('cron-description-text'); 76 | 77 | if (!expression) { 78 | descriptionElement.textContent = 'Enter a cron expression to see description'; 79 | return; 80 | } 81 | 82 | try { 83 | const description = this.describeCronExpression(expression); 84 | descriptionElement.textContent = description; 85 | } catch (error) { 86 | descriptionElement.textContent = 'Invalid cron expression'; 87 | } 88 | } 89 | 90 | describeCronExpression(expression) { 91 | const parts = expression.split(/\s+/); 92 | if (parts.length !== 5) { 93 | throw new Error('Invalid expression'); 94 | } 95 | 96 | const [minute, hour, day, month, dow] = parts; 97 | 98 | let description = 'Runs '; 99 | 100 | const minuteDesc = this.describeField(minute, 'minute', ['past the hour', 'every minute']); 101 | const hourDesc = this.describeField(hour, 'hour', ['', 'every hour']); 102 | const dayDesc = this.describeField(day, 'day', ['on day', 'every day']); 103 | const monthDesc = this.describeField(month, 'month', ['in', 'every month']); 104 | const dowDesc = this.describeField(dow, 'weekday', ['on', 'every day']); 105 | 106 | if (minute === '*' && hour === '*' && day === '*' && month === '*' && dow === '*') { 107 | return 'Runs every minute'; 108 | } 109 | 110 | if (dow !== '*' && day === '*') { 111 | description += `${dowDesc} at ${this.formatTime(hour, minute)}`; 112 | } else if (day !== '*' && dow === '*') { 113 | description += `${dayDesc} of ${monthDesc} at ${this.formatTime(hour, minute)}`; 114 | } else if (day === '*' && dow === '*') { 115 | description += `${hourDesc} ${minuteDesc} ${monthDesc}`; 116 | } else { 117 | description += `${dayDesc} of ${monthDesc} and ${dowDesc} at ${this.formatTime(hour, minute)}`; 118 | } 119 | 120 | return description; 121 | } 122 | 123 | describeField(field, type, prefixes) { 124 | if (field === '*') { 125 | return prefixes[1]; 126 | } 127 | 128 | if (field.includes(',')) { 129 | const values = field.split(','); 130 | return `${prefixes[0]} ${values.join(', ')}`; 131 | } 132 | 133 | if (field.includes('-')) { 134 | const [start, end] = field.split('-'); 135 | return `${prefixes[0]} ${start} through ${end}`; 136 | } 137 | 138 | if (field.includes('/')) { 139 | const [base, step] = field.split('/'); 140 | if (base === '*') { 141 | return `every ${step} ${type}${step > 1 ? 's' : ''}`; 142 | } 143 | return `every ${step} ${type}${step > 1 ? 's' : ''} starting at ${base}`; 144 | } 145 | 146 | return `${prefixes[0]} ${field}`; 147 | } 148 | 149 | formatTime(hour, minute) { 150 | if (hour === '*' && minute === '*') { 151 | return 'every minute'; 152 | } 153 | if (hour === '*') { 154 | return `minute ${minute} of every hour`; 155 | } 156 | if (minute === '*') { 157 | return `every minute of hour ${hour}`; 158 | } 159 | 160 | const h = parseInt(hour); 161 | const m = parseInt(minute); 162 | const period = h >= 12 ? 'PM' : 'AM'; 163 | const displayHour = h === 0 ? 12 : h > 12 ? h - 12 : h; 164 | const displayMinute = m.toString().padStart(2, '0'); 165 | 166 | return `${displayHour}:${displayMinute} ${period}`; 167 | } 168 | 169 | calculateNextRuns() { 170 | const expression = document.getElementById('cron-expression').value.trim(); 171 | const nextRunsList = document.getElementById('next-runs-list'); 172 | 173 | if (!expression) { 174 | nextRunsList.innerHTML = '
  • Enter a cron expression
  • '; 175 | return; 176 | } 177 | 178 | try { 179 | const nextRuns = this.getNextRuns(expression, 5); 180 | nextRunsList.innerHTML = nextRuns.map(date => 181 | `
  • ${date.toLocaleString()}
  • ` 182 | ).join(''); 183 | } catch (error) { 184 | nextRunsList.innerHTML = '
  • Invalid cron expression
  • '; 185 | } 186 | } 187 | 188 | getNextRuns(expression, count) { 189 | const parts = expression.split(/\s+/); 190 | if (parts.length !== 5) { 191 | throw new Error('Invalid expression'); 192 | } 193 | 194 | const [minutePattern, hourPattern, dayPattern, monthPattern, dowPattern] = parts; 195 | const runs = []; 196 | let currentDate = new Date(); 197 | currentDate.setSeconds(0, 0); 198 | currentDate.setMinutes(currentDate.getMinutes() + 1); 199 | 200 | let attempts = 0; 201 | const maxAttempts = 10000; 202 | 203 | while (runs.length < count && attempts < maxAttempts) { 204 | attempts++; 205 | 206 | if (this.matchesCronPattern(currentDate, minutePattern, hourPattern, dayPattern, monthPattern, dowPattern)) { 207 | runs.push(new Date(currentDate)); 208 | } 209 | 210 | currentDate.setMinutes(currentDate.getMinutes() + 1); 211 | } 212 | 213 | return runs; 214 | } 215 | 216 | matchesCronPattern(date, minute, hour, day, month, dow) { 217 | const dateMinute = date.getMinutes(); 218 | const dateHour = date.getHours(); 219 | const dateDay = date.getDate(); 220 | const dateMonth = date.getMonth() + 1; 221 | const dateDow = date.getDay(); 222 | 223 | return this.matchesPattern(dateMinute, minute, 0, 59) && 224 | this.matchesPattern(dateHour, hour, 0, 23) && 225 | this.matchesPattern(dateDay, day, 1, 31) && 226 | this.matchesPattern(dateMonth, month, 1, 12) && 227 | this.matchesPattern(dateDow, dow, 0, 7); 228 | } 229 | 230 | matchesPattern(value, pattern, min, max) { 231 | if (pattern === '*') { 232 | return true; 233 | } 234 | 235 | if (pattern.includes(',')) { 236 | return pattern.split(',').some(p => this.matchesPattern(value, p.trim(), min, max)); 237 | } 238 | 239 | if (pattern.includes('-')) { 240 | const [start, end] = pattern.split('-').map(x => parseInt(x.trim())); 241 | return value >= start && value <= end; 242 | } 243 | 244 | if (pattern.includes('/')) { 245 | const [base, step] = pattern.split('/'); 246 | const stepNum = parseInt(step); 247 | 248 | if (base === '*') { 249 | return (value - min) % stepNum === 0; 250 | } else { 251 | const baseNum = parseInt(base); 252 | return value >= baseNum && (value - baseNum) % stepNum === 0; 253 | } 254 | } 255 | 256 | return value === parseInt(pattern); 257 | } 258 | } 259 | 260 | window.CronCalculator = new CronCalculator(); 261 | 262 | const cronStyles = ` 263 | .cron-fields { 264 | display: grid; 265 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 266 | gap: 15px; 267 | margin-bottom: 20px; 268 | } 269 | 270 | .cron-field { 271 | display: flex; 272 | flex-direction: column; 273 | gap: 5px; 274 | } 275 | 276 | .cron-field label { 277 | font-weight: 500; 278 | color: #1d1d1f; 279 | font-size: 14px; 280 | } 281 | 282 | .cron-field input { 283 | padding: 8px 12px; 284 | border: 2px solid #f0f0f0; 285 | border-radius: 6px; 286 | font-family: 'Monaco', 'Consolas', monospace; 287 | font-size: 14px; 288 | transition: border-color 0.3s ease; 289 | } 290 | 291 | .cron-field input:focus { 292 | outline: none; 293 | border-color: #667eea; 294 | } 295 | 296 | .cron-expression { 297 | margin: 20px 0; 298 | } 299 | 300 | .cron-expression label { 301 | display: block; 302 | font-weight: 500; 303 | color: #1d1d1f; 304 | margin-bottom: 8px; 305 | } 306 | 307 | #cron-expression { 308 | width: 100%; 309 | padding: 12px 15px; 310 | border: 2px solid #f0f0f0; 311 | border-radius: 8px; 312 | font-family: 'Monaco', 'Consolas', monospace; 313 | font-size: 16px; 314 | font-weight: bold; 315 | text-align: center; 316 | background: #f8f9fa; 317 | } 318 | 319 | #cron-expression:focus { 320 | outline: none; 321 | border-color: #667eea; 322 | } 323 | 324 | .cron-description, 325 | .next-runs { 326 | background: #f8f9fa; 327 | border-radius: 8px; 328 | padding: 20px; 329 | margin: 20px 0; 330 | } 331 | 332 | .cron-description h3, 333 | .next-runs h3 { 334 | margin-top: 0; 335 | margin-bottom: 15px; 336 | color: #1d1d1f; 337 | } 338 | 339 | #cron-description-text { 340 | color: #333; 341 | line-height: 1.5; 342 | font-size: 15px; 343 | } 344 | 345 | #next-runs-list { 346 | list-style: none; 347 | padding: 0; 348 | margin: 0; 349 | } 350 | 351 | #next-runs-list li { 352 | background: white; 353 | padding: 10px 15px; 354 | margin: 8px 0; 355 | border-radius: 6px; 356 | border: 1px solid #e0e0e0; 357 | font-family: 'Monaco', 'Consolas', monospace; 358 | font-size: 14px; 359 | color: #333; 360 | } 361 | 362 | .controls { 363 | display: flex; 364 | gap: 15px; 365 | margin: 20px 0; 366 | flex-wrap: wrap; 367 | } 368 | `; 369 | 370 | const cronCalculatorStyle = document.createElement('style'); 371 | style.textContent = cronStyles; 372 | document.head.appendChild(cronCalculatorStyle); -------------------------------------------------------------------------------- /src/renderer/js/tools/hash-generator.js: -------------------------------------------------------------------------------- 1 | class HashGenerator { 2 | constructor() { 3 | this.init(); 4 | } 5 | 6 | init() { 7 | const hashButtons = document.querySelectorAll('.hash-btn'); 8 | const inputTextarea = document.getElementById('hash-input'); 9 | 10 | hashButtons.forEach((button) => { 11 | button.addEventListener('click', () => { 12 | const hashType = button.dataset.type; 13 | this.generateHash(hashType); 14 | }); 15 | }); 16 | 17 | if (inputTextarea) { 18 | inputTextarea.addEventListener('input', () => this.generateAllHashes()); 19 | } 20 | } 21 | 22 | async generateHash(type) { 23 | const input = document.getElementById('hash-input').value; 24 | 25 | if (!input) { 26 | window.app?.showMessage('Please enter text to hash.', 'error'); 27 | return; 28 | } 29 | 30 | try { 31 | const hash = await this.createHash(input, type); 32 | const resultElement = document.querySelector(`#${type}-result textarea`); 33 | 34 | if (resultElement) { 35 | resultElement.value = hash; 36 | this.selectHash(resultElement); 37 | } 38 | 39 | window.app?.showMessage( 40 | `${type.toUpperCase()} hash generated!`, 41 | 'success' 42 | ); 43 | } catch (error) { 44 | window.app?.showMessage( 45 | `Error generating ${type} hash: ${error.message}`, 46 | 'error' 47 | ); 48 | } 49 | } 50 | 51 | async generateAllHashes() { 52 | const input = document.getElementById('hash-input').value; 53 | 54 | if (!input) { 55 | this.clearAllHashes(); 56 | return; 57 | } 58 | 59 | const hashTypes = ['md5', 'sha1', 'sha256', 'sha512']; 60 | 61 | for (const type of hashTypes) { 62 | try { 63 | const hash = await this.createHash(input, type); 64 | const resultElement = document.querySelector( 65 | `#${type}-result textarea` 66 | ); 67 | if (resultElement) { 68 | resultElement.value = hash; 69 | } 70 | } catch (error) { 71 | console.error(`Error generating ${type} hash:`, error); 72 | } 73 | } 74 | } 75 | 76 | async createHash(input, algorithm) { 77 | const encoder = new TextEncoder(); 78 | const data = encoder.encode(input); 79 | 80 | let hashBuffer; 81 | switch (algorithm) { 82 | case 'md5': 83 | return this.md5(input); 84 | case 'sha1': 85 | hashBuffer = await crypto.subtle.digest('SHA-1', data); 86 | break; 87 | case 'sha256': 88 | hashBuffer = await crypto.subtle.digest('SHA-256', data); 89 | break; 90 | case 'sha512': 91 | hashBuffer = await crypto.subtle.digest('SHA-512', data); 92 | break; 93 | default: 94 | throw new Error('Unsupported hash algorithm'); 95 | } 96 | 97 | return Array.from(new Uint8Array(hashBuffer)) 98 | .map((b) => b.toString(16).padStart(2, '0')) 99 | .join(''); 100 | } 101 | 102 | md5(input) { 103 | function rotateLeft(lValue, iShiftBits) { 104 | return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); 105 | } 106 | 107 | function addUnsigned(lX, lY) { 108 | const lX4 = lX & 0x40000000; 109 | const lY4 = lY & 0x40000000; 110 | const lX8 = lX & 0x80000000; 111 | const lY8 = lY & 0x80000000; 112 | const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff); 113 | if (lX4 & lY4) { 114 | return lResult ^ 0x80000000 ^ lX8 ^ lY8; 115 | } 116 | if (lX4 | lY4) { 117 | if (lResult & 0x40000000) { 118 | return lResult ^ 0xc0000000 ^ lX8 ^ lY8; 119 | } else { 120 | return lResult ^ 0x40000000 ^ lX8 ^ lY8; 121 | } 122 | } else { 123 | return lResult ^ lX8 ^ lY8; 124 | } 125 | } 126 | 127 | function F(x, y, z) { 128 | return (x & y) | (~x & z); 129 | } 130 | function G(x, y, z) { 131 | return (x & z) | (y & ~z); 132 | } 133 | function H(x, y, z) { 134 | return x ^ y ^ z; 135 | } 136 | function I(x, y, z) { 137 | return y ^ (x | ~z); 138 | } 139 | 140 | function FF(a, b, c, d, x, s, ac) { 141 | a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac)); 142 | return addUnsigned(rotateLeft(a, s), b); 143 | } 144 | 145 | function GG(a, b, c, d, x, s, ac) { 146 | a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac)); 147 | return addUnsigned(rotateLeft(a, s), b); 148 | } 149 | 150 | function HH(a, b, c, d, x, s, ac) { 151 | a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac)); 152 | return addUnsigned(rotateLeft(a, s), b); 153 | } 154 | 155 | function II(a, b, c, d, x, s, ac) { 156 | a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac)); 157 | return addUnsigned(rotateLeft(a, s), b); 158 | } 159 | 160 | function convertToWordArray(string) { 161 | let lWordCount; 162 | const lMessageLength = string.length; 163 | const lNumberOfWordsTemp1 = lMessageLength + 8; 164 | const lNumberOfWordsTemp2 = 165 | (lNumberOfWordsTemp1 - (lNumberOfWordsTemp1 % 64)) / 64; 166 | const lNumberOfWords = (lNumberOfWordsTemp2 + 1) * 16; 167 | const lWordArray = Array(lNumberOfWords - 1); 168 | let lBytePosition = 0; 169 | let lByteCount = 0; 170 | while (lByteCount < lMessageLength) { 171 | lWordCount = (lByteCount - (lByteCount % 4)) / 4; 172 | lBytePosition = (lByteCount % 4) * 8; 173 | lWordArray[lWordCount] = 174 | lWordArray[lWordCount] | 175 | (string.charCodeAt(lByteCount) << lBytePosition); 176 | lByteCount++; 177 | } 178 | lWordCount = (lByteCount - (lByteCount % 4)) / 4; 179 | lBytePosition = (lByteCount % 4) * 8; 180 | lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); 181 | lWordArray[lNumberOfWords - 2] = lMessageLength << 3; 182 | lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; 183 | return lWordArray; 184 | } 185 | 186 | function wordToHex(lValue) { 187 | let wordToHexValue = ''; 188 | let wordToHexValueTemp = ''; 189 | let lByte, lCount; 190 | for (lCount = 0; lCount <= 3; lCount++) { 191 | lByte = (lValue >>> (lCount * 8)) & 255; 192 | wordToHexValueTemp = '0' + lByte.toString(16); 193 | wordToHexValue = 194 | wordToHexValue + 195 | wordToHexValueTemp.substring(wordToHexValueTemp.length - 2); 196 | } 197 | return wordToHexValue; 198 | } 199 | 200 | let x = convertToWordArray(input); 201 | let a = 0x67452301; 202 | let b = 0xefcdab89; 203 | let c = 0x98badcfe; 204 | let d = 0x10325476; 205 | 206 | for (let k = 0; k < x.length; k += 16) { 207 | const AA = a; 208 | const BB = b; 209 | const CC = c; 210 | const DD = d; 211 | a = FF(a, b, c, d, x[k + 0], 7, 0xd76aa478); 212 | d = FF(d, a, b, c, x[k + 1], 12, 0xe8c7b756); 213 | c = FF(c, d, a, b, x[k + 2], 17, 0x242070db); 214 | b = FF(b, c, d, a, x[k + 3], 22, 0xc1bdceee); 215 | a = FF(a, b, c, d, x[k + 4], 7, 0xf57c0faf); 216 | d = FF(d, a, b, c, x[k + 5], 12, 0x4787c62a); 217 | c = FF(c, d, a, b, x[k + 6], 17, 0xa8304613); 218 | b = FF(b, c, d, a, x[k + 7], 22, 0xfd469501); 219 | a = FF(a, b, c, d, x[k + 8], 7, 0x698098d8); 220 | d = FF(d, a, b, c, x[k + 9], 12, 0x8b44f7af); 221 | c = FF(c, d, a, b, x[k + 10], 17, 0xffff5bb1); 222 | b = FF(b, c, d, a, x[k + 11], 22, 0x895cd7be); 223 | a = FF(a, b, c, d, x[k + 12], 7, 0x6b901122); 224 | d = FF(d, a, b, c, x[k + 13], 12, 0xfd987193); 225 | c = FF(c, d, a, b, x[k + 14], 17, 0xa679438e); 226 | b = FF(b, c, d, a, x[k + 15], 22, 0x49b40821); 227 | a = GG(a, b, c, d, x[k + 1], 5, 0xf61e2562); 228 | d = GG(d, a, b, c, x[k + 6], 9, 0xc040b340); 229 | c = GG(c, d, a, b, x[k + 11], 14, 0x265e5a51); 230 | b = GG(b, c, d, a, x[k + 0], 20, 0xe9b6c7aa); 231 | a = GG(a, b, c, d, x[k + 5], 5, 0xd62f105d); 232 | d = GG(d, a, b, c, x[k + 10], 9, 0x2441453); 233 | c = GG(c, d, a, b, x[k + 15], 14, 0xd8a1e681); 234 | b = GG(b, c, d, a, x[k + 4], 20, 0xe7d3fbc8); 235 | a = GG(a, b, c, d, x[k + 9], 5, 0x21e1cde6); 236 | d = GG(d, a, b, c, x[k + 14], 9, 0xc33707d6); 237 | c = GG(c, d, a, b, x[k + 3], 14, 0xf4d50d87); 238 | b = GG(b, c, d, a, x[k + 8], 20, 0x455a14ed); 239 | a = GG(a, b, c, d, x[k + 13], 5, 0xa9e3e905); 240 | d = GG(d, a, b, c, x[k + 2], 9, 0xfcefa3f8); 241 | c = GG(c, d, a, b, x[k + 7], 14, 0x676f02d9); 242 | b = GG(b, c, d, a, x[k + 12], 20, 0x8d2a4c8a); 243 | a = HH(a, b, c, d, x[k + 5], 4, 0xfffa3942); 244 | d = HH(d, a, b, c, x[k + 8], 11, 0x8771f681); 245 | c = HH(c, d, a, b, x[k + 11], 16, 0x6d9d6122); 246 | b = HH(b, c, d, a, x[k + 14], 23, 0xfde5380c); 247 | a = HH(a, b, c, d, x[k + 1], 4, 0xa4beea44); 248 | d = HH(d, a, b, c, x[k + 4], 11, 0x4bdecfa9); 249 | c = HH(c, d, a, b, x[k + 7], 16, 0xf6bb4b60); 250 | b = HH(b, c, d, a, x[k + 10], 23, 0xbebfbc70); 251 | a = HH(a, b, c, d, x[k + 13], 4, 0x289b7ec6); 252 | d = HH(d, a, b, c, x[k + 0], 11, 0xeaa127fa); 253 | c = HH(c, d, a, b, x[k + 3], 16, 0xd4ef3085); 254 | b = HH(b, c, d, a, x[k + 6], 23, 0x4881d05); 255 | a = HH(a, b, c, d, x[k + 9], 4, 0xd9d4d039); 256 | d = HH(d, a, b, c, x[k + 12], 11, 0xe6db99e5); 257 | c = HH(c, d, a, b, x[k + 15], 16, 0x1fa27cf8); 258 | b = HH(b, c, d, a, x[k + 2], 23, 0xc4ac5665); 259 | a = II(a, b, c, d, x[k + 0], 6, 0xf4292244); 260 | d = II(d, a, b, c, x[k + 7], 10, 0x432aff97); 261 | c = II(c, d, a, b, x[k + 14], 15, 0xab9423a7); 262 | b = II(b, c, d, a, x[k + 5], 21, 0xfc93a039); 263 | a = II(a, b, c, d, x[k + 12], 6, 0x655b59c3); 264 | d = II(d, a, b, c, x[k + 3], 10, 0x8f0ccc92); 265 | c = II(c, d, a, b, x[k + 10], 15, 0xffeff47d); 266 | b = II(b, c, d, a, x[k + 1], 21, 0x85845dd1); 267 | a = II(a, b, c, d, x[k + 8], 6, 0x6fa87e4f); 268 | d = II(d, a, b, c, x[k + 15], 10, 0xfe2ce6e0); 269 | c = II(c, d, a, b, x[k + 6], 15, 0xa3014314); 270 | b = II(b, c, d, a, x[k + 13], 21, 0x4e0811a1); 271 | a = II(a, b, c, d, x[k + 4], 6, 0xf7537e82); 272 | d = II(d, a, b, c, x[k + 11], 10, 0xbd3af235); 273 | c = II(c, d, a, b, x[k + 2], 15, 0x2ad7d2bb); 274 | b = II(b, c, d, a, x[k + 9], 21, 0xeb86d391); 275 | a = addUnsigned(a, AA); 276 | b = addUnsigned(b, BB); 277 | c = addUnsigned(c, CC); 278 | d = addUnsigned(d, DD); 279 | } 280 | 281 | const temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d); 282 | return temp.toLowerCase(); 283 | } 284 | 285 | selectHash(element) { 286 | setTimeout(() => { 287 | element.select(); 288 | element.setSelectionRange(0, 99999); 289 | }, 100); 290 | } 291 | 292 | clearAllHashes() { 293 | const hashTypes = ['md5', 'sha1', 'sha256', 'sha512']; 294 | hashTypes.forEach((type) => { 295 | const resultElement = document.querySelector(`#${type}-result textarea`); 296 | if (resultElement) { 297 | resultElement.value = ''; 298 | } 299 | }); 300 | } 301 | 302 | copyHash(type) { 303 | const resultElement = document.querySelector(`#${type}-result textarea`); 304 | if (resultElement && resultElement.value) { 305 | navigator.clipboard.writeText(resultElement.value).then(() => { 306 | window.app?.showMessage( 307 | `${type.toUpperCase()} hash copied!`, 308 | 'success' 309 | ); 310 | }); 311 | } 312 | } 313 | } 314 | 315 | window.HashGenerator = new HashGenerator(); 316 | 317 | const hashStyles = ` 318 | .hash-options { 319 | display: flex; 320 | flex-wrap: wrap; 321 | gap: 10px; 322 | margin: 20px 0; 323 | } 324 | 325 | .hash-btn { 326 | background: #f0f0f0; 327 | border: 1px solid #d0d0d0; 328 | padding: 10px 16px; 329 | border-radius: 6px; 330 | cursor: pointer; 331 | font-size: 14px; 332 | transition: all 0.3s ease; 333 | } 334 | 335 | .hash-btn:hover { 336 | background: #667eea; 337 | color: white; 338 | border-color: #667eea; 339 | } 340 | 341 | .hash-results { 342 | display: grid; 343 | gap: 15px; 344 | margin-top: 20px; 345 | } 346 | 347 | .hash-result { 348 | display: flex; 349 | flex-direction: column; 350 | gap: 5px; 351 | } 352 | 353 | .hash-result label { 354 | font-weight: 500; 355 | color: #1d1d1f; 356 | font-size: 14px; 357 | } 358 | 359 | .hash-result textarea { 360 | font-family: 'Monaco', 'Consolas', monospace; 361 | font-size: 12px; 362 | padding: 10px; 363 | border: 2px solid #f0f0f0; 364 | border-radius: 6px; 365 | resize: none; 366 | height: 60px; 367 | background-color: #fafafa; 368 | } 369 | `; 370 | 371 | const hashGeneratorStyle = document.createElement('style'); 372 | hashGeneratorStyle.textContent = hashGeneratorStyles; 373 | document.head.appendChild(hashGeneratorStyle); 374 | --------------------------------------------------------------------------------