diff --git a/.gitignore b/.gitignore index 1985dd7..44fd20b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules package-lock.json -output/ \ No newline at end of file +output/ +**/*.js.map \ No newline at end of file diff --git a/TYPESCRIPT_MIGRATION.md b/TYPESCRIPT_MIGRATION.md new file mode 100644 index 0000000..a3c0233 --- /dev/null +++ b/TYPESCRIPT_MIGRATION.md @@ -0,0 +1,219 @@ +# TypeScript Migration Guide for Pegasus + +This document provides guidance for continuing the TypeScript migration of the Pegasus Electron application. + +## Current Status + +### ✅ Completed +1. **TypeScript Environment Setup** + - TypeScript 5.9.3 installed + - tsconfig.json configured for gradual migration + - Compilation system set up with selective build script + - Source maps enabled for debugging + +2. **Type Definitions** + - Comprehensive `types.d.ts` with IPC channels, image types, and UI types + - Full Electron type support (built-in with Electron 37) + - Type-safe IPC communication definitions + +3. **Core Process Migration** + - `main.ts` - Main Electron process (fully typed) + - `utils/ipc_bridge.ts` - IPC communication bridge (fully typed) + +### 🔄 Remaining Files to Migrate + +#### Priority 1: Processing Layer (Independent utilities) +- `processing/image_processor.js` - Image processing operations using Sharp +- `processing/image_loader.js` - File I/O for images +- `processing/format_converter.js` - Format conversion (PNG, SVG, BMP, ICO, PIX, HEIF) + +#### Priority 2: Core Classes +- `core/pix.js` - PIX format handling +- `core/image_layer.js` - Image layer management (~600 lines) +- `core/image_renderer.js` - Canvas rendering and UI + +#### Priority 3: Feature Modules +- `features/image_layer_events.js` - Event handlers for layers +- `features/layer_history.js` - Undo/redo functionality +- `features/image_mode.js` - Mode management (crop, draw, etc.) +- `features/gif_animation.js` - GIF animation support + +#### Priority 4: Renderer Processes +- `renderer/main_renderer.js` - Main UI renderer +- `renderer/paint_renderer.js` - Paint tool UI +- `renderer/filter_renderer.js` - Filter UI +- `renderer/resize_renderer.js` - Resize UI +- `renderer/rotate_renderer.js` - Rotate UI +- `renderer/crop_renderer.js` - Crop UI +- `renderer/image_analysis_renderer.js` - Analysis UI + +#### Priority 5: Main Application +- `image_kit.js` - Main application controller +- `utils/ui_loader.js` - UI loading utilities + +## Migration Workflow + +### Step 1: Convert a File to TypeScript + +```bash +# Copy the JavaScript file to TypeScript +cp path/to/file.js path/to/file.ts + +# Edit the file and add types +# - Add import statements (replace require with import) +# - Add type annotations to function parameters +# - Add return type annotations +# - Add interface/type definitions for complex objects +``` + +### Step 2: Update Types + +When converting a file, add any new types to `types.d.ts` if they'll be reused across files. + +### Step 3: Compile and Test + +```bash +# Compile TypeScript +npm run compile + +# Run the application +npm start + +# Build for production +npm run build +``` + +### Step 4: Remove Old JavaScript File + +Once the TypeScript version is working: +```bash +rm path/to/file.js +``` + +## TypeScript Best Practices for This Project + +### 1. Gradual Migration +- Convert files leaf-first (files with fewer dependencies first) +- Keep both .js and .ts files during transition +- Test after each conversion + +### 2. Type Annotations +```typescript +// ✅ Good - Explicit types +function processImage(buffer: Buffer, options: ImageProcessOptions): Promise { + // ... +} + +// ❌ Avoid - Implicit any +function processImage(buffer, options) { + // ... +} +``` + +### 3. Use Existing Types +```typescript +import { ImageInfo, ImageProcessOptions } from '../types'; + +// Use defined types from types.d.ts +``` + +### 4. IPC Communication +```typescript +// Use typed IPC handlers +ipcMain.on('channel-name', (event: IpcMainEvent, arg: string) => { + // Handler logic +}); +``` + +### 5. Async/Await +```typescript +// ✅ Prefer async/await +async function loadImage(path: string): Promise { + const buffer = await fs.promises.readFile(path); + return buffer; +} + +// ❌ Avoid callback-style when possible +fs.readFile(path, (err, buffer) => { + // ... +}); +``` + +## Troubleshooting + +### Issue: TypeScript compilation errors +**Solution:** Check that: +1. All imports are correctly typed +2. Required type definitions are in `types.d.ts` +3. No circular dependencies exist + +### Issue: Module not found errors at runtime +**Solution:** Ensure: +1. Compiled .js files exist alongside .ts files +2. Module paths are correct (use `.js` extension in imports for compiled output) + +### Issue: Type conflicts with Electron +**Solution:** +- Electron 37+ provides built-in types +- Do not install `@types/electron` +- Use `skipLibCheck: true` in tsconfig.json if needed + +## Strictness Roadmap + +### Phase 1 (Current): Lenient Mode +```json +{ + "strict": false, + "noImplicitAny": false +} +``` + +### Phase 2: Enable Implicit Any Checking +After most files are migrated: +```json +{ + "strict": false, + "noImplicitAny": true +} +``` + +### Phase 3: Full Strict Mode +When all files are typed: +```json +{ + "strict": true +} +``` + +## Testing Strategy + +### Before Migration +1. Document current behavior +2. Take screenshots of UI +3. Test all major features + +### After Each File +1. Compile successfully +2. Run the app and test affected features +3. Check for console errors +4. Verify IPC communication still works + +### Before Enabling Strict Mode +1. Run full regression testing +2. Test all image operations +3. Test file save/load +4. Test clipboard operations +5. Test all UI panels + +## Resources + +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) +- [Electron TypeScript Guide](https://www.electronjs.org/docs/latest/tutorial/typescript) +- [Sharp TypeScript Types](https://sharp.pixelplumbing.com/api-constructor) + +## Questions? + +For questions about this migration: +1. Check existing TypeScript files (main.ts, ipc_bridge.ts) for patterns +2. Review types.d.ts for available type definitions +3. Consult TypeScript documentation for specific type issues diff --git a/compile-ts.sh b/compile-ts.sh new file mode 100755 index 0000000..a9f8272 --- /dev/null +++ b/compile-ts.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Compile only TypeScript files that don't have a corresponding .js file + +# Find all .ts files (excluding .d.ts files) +find . -name "*.ts" -not -name "*.d.ts" -not -path "./node_modules/*" -not -path "./output/*" | while read -r tsfile; do + # Get the corresponding .js filename + jsfile="${tsfile%.ts}.js" + + # Check if .ts file was modified more recently than .js or if .js doesn't exist + if [ ! -f "$jsfile" ] || [ "$tsfile" -nt "$jsfile" ]; then + echo "Compiling: $tsfile" + # Use tsconfig.json settings with just the specific file + npx tsc "$tsfile" 2>/dev/null + fi +done + +echo "TypeScript compilation completed" +exit 0 diff --git a/main.js b/main.js index e4c8f24..7ece8e7 100644 --- a/main.js +++ b/main.js @@ -1,371 +1,358 @@ -const electron = require("electron"); -const pkg = require("./package.json"); -const { - app, - ipcMain, - dialog, - BrowserWindow, - BrowserView, - Menu, - MenuItem, - clipboard, - nativeImage, -} = electron; -const { ImageProcessor } = require("./processing/image_processor"); -const { IPCBridge } = require("./utils/ipc_bridge"); - -//electron refresh (only develop) -if (process.env.NODE_ENV === "development") { - try { - require("electron-reload")(__dirname, { - electron: require(`${__dirname}/node_modules/electron`), - }); - } catch (e) { - console.log("electron-reload not found (this is okay in production)"); - } +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const electron_1 = require("electron"); +const path = __importStar(require("path")); +const image_processor_1 = require("./processing/image_processor"); +const ipc_bridge_1 = require("./utils/ipc_bridge"); +// Import package.json for app metadata +const pkg = require('./package.json'); +// Electron refresh (only in development) +if (process.env.NODE_ENV === 'development') { + try { + require('electron-reload')(__dirname, { + electron: require(`${__dirname}/node_modules/electron`), + }); + } + catch (e) { + console.log('electron-reload not found (this is okay in production)'); + } } - // Configuration for browser window creation const WINDOW_CONFIG = { - width: 1280, - height: 1024, - center: true, - icon: `${__dirname}/assets/icon.ico`, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - }, + width: 1280, + height: 1024, + center: true, + icon: path.join(__dirname, 'assets', 'icon.ico'), + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, }; - function createMainWindow() { - const mainWindow = new BrowserWindow(WINDOW_CONFIG); - mainWindow.loadFile("index.html"); - return mainWindow; + const mainWindow = new electron_1.BrowserWindow(WINDOW_CONFIG); + mainWindow.loadFile('index.html'); + return mainWindow; } - function createPanelView(mainWindow) { - const view = new BrowserView({ - resizable: true, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - }, - }); - - view.setBounds({ x: 0, y: 33, width: 1280, height: 90 }); - view.setAutoResize({ width: true, height: false }); - view.webContents.loadFile(`./pages/_panel.html`); - mainWindow.addBrowserView(view); + const view = new electron_1.BrowserView({ + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, + }); + view.setBounds({ x: 0, y: 33, width: 1280, height: 90 }); + view.setAutoResize({ width: true, height: false }); + view.webContents.loadFile('./pages/_panel.html'); + mainWindow.addBrowserView(view); } - // Context menu helper function createContextMenu(event, { hasUndo, hasRedo }) { - const contextMenuItems = [ - { label: "Copy", accelerator: "Ctrl+C", action: "copy" }, - { label: "Paste", accelerator: "Ctrl+V", action: "paste" }, - { type: "separator" }, - { label: "Delete", accelerator: "Delete", action: "delete" }, - { type: "separator" }, - { label: "Undo", accelerator: "Ctrl+Z", action: "undo", enabled: hasUndo }, - { label: "Redo", accelerator: "Ctrl+Y", action: "redo", enabled: hasRedo }, - { - label: "Watermark", - accelerator: "Ctrl+W", - action: "watermark", - enabled: true, - }, - { type: "separator" }, - { label: "Hide", accelerator: "Alt + H", action: "hide" }, - ]; - - const menu = new Menu(); - contextMenuItems.forEach((item) => { - if (item.type === "separator") { - menu.append(new MenuItem({ type: "separator" })); - } else { - menu.append( - new MenuItem({ - label: item.label, - accelerator: item.accelerator, - enabled: item.enabled !== false, - click: () => { - event.sender.send("imgkit-context-menu-action", item.action); - }, - }) - ); - } - }); - - return menu; + const contextMenuItems = [ + { label: 'Copy', accelerator: 'Ctrl+C', action: 'copy' }, + { label: 'Paste', accelerator: 'Ctrl+V', action: 'paste' }, + { type: 'separator' }, + { label: 'Delete', accelerator: 'Delete', action: 'delete' }, + { type: 'separator' }, + { label: 'Undo', accelerator: 'Ctrl+Z', action: 'undo', enabled: hasUndo }, + { label: 'Redo', accelerator: 'Ctrl+Y', action: 'redo', enabled: hasRedo }, + { + label: 'Watermark', + accelerator: 'Ctrl+W', + action: 'watermark', + enabled: true, + }, + { type: 'separator' }, + { label: 'Hide', accelerator: 'Alt + H', action: 'hide' }, + ]; + const menu = new electron_1.Menu(); + contextMenuItems.forEach((item) => { + if (item.type === 'separator') { + menu.append(new electron_1.MenuItem({ type: 'separator' })); + } + else { + menu.append(new electron_1.MenuItem({ + label: item.label, + accelerator: item.accelerator, + enabled: item.enabled !== false, + click: () => { + event.sender.send('imgkit-context-menu-action', item.action); + }, + })); + } + }); + return menu; } - // Application menu helper function buildApplicationMenu(event, mainWindow) { - const isDev = process.env.NODE_ENV === "development" || !app.isPackaged; - - const IMAGE_EXTENSIONS = [ - "pix", - "png", - "svg", - "jpg", - "jpeg", - "webp", - "bmp", - "gif", - "ico", - "tiff", - "tif", - "avif", - "heif", - "heic", - ]; - - const template = [ - { - label: "File", - submenu: [ + const isDev = process.env.NODE_ENV === 'development' || !electron_1.app.isPackaged; + const IMAGE_EXTENSIONS = [ + 'pix', + 'png', + 'svg', + 'jpg', + 'jpeg', + 'webp', + 'bmp', + 'gif', + 'ico', + 'tiff', + 'tif', + 'avif', + 'heif', + 'heic', + ]; + const template = [ { - label: "Open...", - accelerator: "Ctrl+O", - click: () => { - dialog - .showOpenDialog({ - properties: ["openFile", "multiSelections"], - filters: [{ name: "Image file", extensions: IMAGE_EXTENSIONS }], - }) - .then((result) => { - event.sender.send("openImgCMD", result.filePaths); - }); - }, - }, - { - label: "Save", - accelerator: "Ctrl+S", - click: () => event.sender.send("saveImgCMD"), - }, - { - label: "Save As...", - accelerator: "Ctrl+Shift+S", - click: () => event.sender.send("setExtensionCMD"), - }, - ], - }, - ...(isDev - ? [ - { - label: "Debug", + label: 'File', submenu: [ - { - label: "Toggle Developer Tools", - accelerator: "F12", - click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.openDevTools(); - } + { + label: 'Open...', + accelerator: 'Ctrl+O', + click: async () => { + const result = await electron_1.dialog.showOpenDialog(mainWindow, { + properties: ['openFile', 'multiSelections'], + filters: [{ name: 'Image file', extensions: IMAGE_EXTENSIONS }], + }); + event.sender.send('openImgCMD', result.filePaths); + }, }, - }, - { - label: "Toggle BrowserView Developer Tools", - accelerator: "Ctrl+Shift+I", - click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - const views = mainWindow.getBrowserViews(); - if (views.length > 0) { - views[0].webContents.openDevTools(); - } - } + { + label: 'Save', + accelerator: 'Ctrl+S', + click: () => event.sender.send('saveImgCMD'), + }, + { + label: 'Save As...', + accelerator: 'Ctrl+Shift+S', + click: () => event.sender.send('setExtensionCMD'), }, - }, ], - }, - ] - : []), - { - label: "Help", - submenu: [ + }, + ...(isDev + ? [ + { + label: 'Debug', + submenu: [ + { + label: 'Toggle Developer Tools', + accelerator: 'F12', + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.openDevTools(); + } + }, + }, + { + label: 'Toggle BrowserView Developer Tools', + accelerator: 'Ctrl+Shift+I', + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + const views = mainWindow.getBrowserViews(); + if (views.length > 0) { + views[0].webContents.openDevTools(); + } + } + }, + }, + ], + }, + ] + : []), { - label: "About", - click: () => { - dialog.showMessageBox({ - title: "About", - buttons: ["Ok"], - message: `Author : ${pkg.author.name}\nEmail : ${pkg.author.email}\nVersion : v${pkg.version}\nLicense : ${pkg.license}\n`, - }); - }, + label: 'Help', + submenu: [ + { + label: 'About', + click: () => { + electron_1.dialog.showMessageBox({ + title: 'About', + buttons: ['Ok'], + message: `Author : ${pkg.author.name}\nEmail : ${pkg.author.email}\nVersion : v${pkg.version}\nLicense : ${pkg.license}\n`, + }); + }, + }, + ], }, - ], - }, - ]; - - return Menu.buildFromTemplate(template); + ]; + return electron_1.Menu.buildFromTemplate(template); } - // This method will be called when Electron has finished initialization -app.whenReady().then(() => { - Menu.setApplicationMenu(null); - - const mainWindow = createMainWindow(); - - // Initialize IPCBridge instance - const ipcBridge = new IPCBridge(ipcMain, mainWindow); - - // Panel loading handlers - load panel for various image operations - const PANEL_OPERATIONS = [ - "resizeImgREQ", - "cropImgREQ", - "filterImgREQ", - "rotateImgREQ", - "paintImgREQ", - "image_analysisImgREQ", - ]; - - PANEL_OPERATIONS.forEach((operation) => { - ipcMain.on(operation, () => { - const panelName = operation.replace("ImgREQ", ""); - mainWindow - .getBrowserViews()[0] - .webContents.loadFile(`./pages/${panelName}_panel.html`); +electron_1.app.whenReady().then(() => { + electron_1.Menu.setApplicationMenu(null); + const mainWindow = createMainWindow(); + // Initialize IPCBridge instance + const ipcBridge = new ipc_bridge_1.IPCBridge(electron_1.ipcMain, mainWindow); + // Panel loading handlers - load panel for various image operations + const PANEL_OPERATIONS = [ + 'resizeImgREQ', + 'cropImgREQ', + 'filterImgREQ', + 'rotateImgREQ', + 'paintImgREQ', + 'image_analysisImgREQ', + ]; + PANEL_OPERATIONS.forEach((operation) => { + electron_1.ipcMain.on(operation, () => { + const panelName = operation.replace('ImgREQ', ''); + mainWindow + .getBrowserViews()[0] + .webContents.loadFile(`./pages/${panelName}_panel.html`); + }); }); - }); - - // IPC Forwarders - Register simple forwarding patterns in bulk - ipcBridge.registerMultiple([ - // Resize commands - { from: "resizeValueSEND", to: "resizeImgCMD" }, - { from: "resizePixelValueSEND", to: "resizePixelImgCMD" }, - - // Filter commands - { from: "blurValueSEND", to: "blurImgCMD" }, - { from: "sharpenValueSEND", to: "sharpenImgCMD" }, - { from: "normalizeImgREQ", to: "normalizeImgCMD" }, - { from: "medianValueSEND", to: "medianImgCMD" }, - { from: "dilateValueSEND", to: "dilateImgCMD" }, - { from: "erodeValueSEND", to: "erodeImgCMD" }, - { from: "bitwiseValueSEND", to: "bitwiseImgCMD" }, - { from: "negativeImgREQ", to: "negativeImgCMD" }, - { from: "grayScaleImgREQ", to: "grayScaleImgCMD" }, - - // Rotate commands - { from: "rotateValueSEND", to: "rotateImgCMD" }, - { from: "rotateLeftImgREQ", to: "rotateLeftImgCMD" }, - { from: "rotateRightImgREQ", to: "rotateRightImgCMD" }, - { from: "flipImgREQ", to: "flipImgCMD" }, - { from: "flopImgREQ", to: "flopImgCMD" }, - - // Paint commands - { from: "tintValueSEND", to: "tintImgCMD" }, - { from: "colorpickerImgREQ", to: "colorpickerImgCMD" }, - { from: "watermarkUploadREQ", to: "watermarkUploadCMD" }, - { from: "watermarkImgREQ", to: "watermarkImgCMD" }, - { from: "drawImgREQ", to: "drawImgCMD" }, - { from: "padImgREQ", to: "padImgCMD" }, - - // Crop commands - { from: "rectCropImgREQ", to: "cropImgCMD" }, - ]); - - // Custom handler - colorpicker requires broadcast to BrowserViews - ipcMain.on("colorpickerValueSEND", async (event, color) => { - const views = mainWindow.getBrowserViews(); - const color_name = await ImageProcessor.getColorName(color); - views.forEach((view) => { - view.webContents.send("colorpickerValueRECV", color, color_name); + // IPC Forwarders - Register simple forwarding patterns in bulk + ipcBridge.registerMultiple([ + // Resize commands + { from: 'resizeValueSEND', to: 'resizeImgCMD' }, + { from: 'resizePixelValueSEND', to: 'resizePixelImgCMD' }, + // Filter commands + { from: 'blurValueSEND', to: 'blurImgCMD' }, + { from: 'sharpenValueSEND', to: 'sharpenImgCMD' }, + { from: 'normalizeImgREQ', to: 'normalizeImgCMD' }, + { from: 'medianValueSEND', to: 'medianImgCMD' }, + { from: 'dilateValueSEND', to: 'dilateImgCMD' }, + { from: 'erodeValueSEND', to: 'erodeImgCMD' }, + { from: 'bitwiseValueSEND', to: 'bitwiseImgCMD' }, + { from: 'negativeImgREQ', to: 'negativeImgCMD' }, + { from: 'grayScaleImgREQ', to: 'grayScaleImgCMD' }, + // Rotate commands + { from: 'rotateValueSEND', to: 'rotateImgCMD' }, + { from: 'rotateLeftImgREQ', to: 'rotateLeftImgCMD' }, + { from: 'rotateRightImgREQ', to: 'rotateRightImgCMD' }, + { from: 'flipImgREQ', to: 'flipImgCMD' }, + { from: 'flopImgREQ', to: 'flopImgCMD' }, + // Paint commands + { from: 'tintValueSEND', to: 'tintImgCMD' }, + { from: 'colorpickerImgREQ', to: 'colorpickerImgCMD' }, + { from: 'watermarkUploadREQ', to: 'watermarkUploadCMD' }, + { from: 'watermarkImgREQ', to: 'watermarkImgCMD' }, + { from: 'drawImgREQ', to: 'drawImgCMD' }, + { from: 'padImgREQ', to: 'padImgCMD' }, + // Crop commands + { from: 'rectCropImgREQ', to: 'cropImgCMD' }, + ]); + // Custom handler - colorpicker requires broadcast to BrowserViews + electron_1.ipcMain.on('colorpickerValueSEND', async (event, color) => { + const views = mainWindow.getBrowserViews(); + const color_name = await image_processor_1.ImageProcessor.getColorName(color); + views.forEach((view) => { + view.webContents.send('colorpickerValueRECV', color, color_name); + }); + }); + // Notification Handler - Forward to main window + electron_1.ipcMain.on('showNotificationREQ', (event, notificationId) => { + mainWindow.webContents.send('showNotificationCMD', notificationId); + }); + // ImgKit Context Menu Handler + electron_1.ipcMain.on('show-imgkit-context-menu', (event, options) => { + const menu = createContextMenu(event, options); + menu.popup({ window: electron_1.BrowserWindow.fromWebContents(event.sender) }); + }); + // Native Clipboard Handlers + electron_1.ipcMain.on('copy-image-to-clipboard', (event, imageBuffer) => { + try { + const image = electron_1.nativeImage.createFromBuffer(Buffer.from(imageBuffer)); + electron_1.clipboard.writeImage(image); + } + catch (error) { + console.error('Failed to copy image to clipboard:', error); + } + }); + electron_1.ipcMain.on('imgkit-layer-event', (event, payload) => { + event.sender.send('imgkit-layer-event', payload); + }); + electron_1.ipcMain.handle('paste-image-from-clipboard', () => { + try { + const image = electron_1.clipboard.readImage(); + if (!image.isEmpty()) { + return image.toPNG(); + } + return null; + } + catch (error) { + console.error('Failed to paste image from clipboard:', error); + return null; + } + }); + electron_1.ipcMain.on('FullScreenREQ', (event) => { + mainWindow.setSimpleFullScreen(true); + mainWindow.show(); + }); + electron_1.ipcMain.on('DefaultScreenREQ', (event) => { + mainWindow.setSimpleFullScreen(false); + mainWindow.show(); + }); + electron_1.ipcMain.on('extensionValueSEND', async (event, res) => { + const result = await electron_1.dialog.showSaveDialog(mainWindow, { + title: 'Save Image', + defaultPath: '~/image', + filters: [ + { + name: 'Image file', + extensions: [res], + }, + ], + }); + event.sender.send('saveAsImgCMD', result.filePath); + }); + electron_1.ipcMain.on('saveImgREQ', (event) => { + event.sender.send('saveImgCMD'); + }); + electron_1.ipcMain.on('deleteImgREQ', (event) => { + event.sender.send('deleteImgCMD'); + }); + // Show application menu + electron_1.ipcMain.on('showMenuREQ', (event) => { + const menu = buildApplicationMenu(event, mainWindow); + electron_1.Menu.setApplicationMenu(menu); + createPanelView(mainWindow); + }); + electron_1.app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (electron_1.BrowserWindow.getAllWindows().length === 0) + createMainWindow(); }); - }); - - // Notification Handler - Forward to main window - ipcMain.on("showNotificationREQ", (event, notificationId) => { - mainWindow.webContents.send("showNotificationCMD", notificationId); - }); - - // ImgKit Context Menu Handler - ipcMain.on("show-imgkit-context-menu", (event, options) => { - const menu = createContextMenu(event, options); - menu.popup({ window: BrowserWindow.fromWebContents(event.sender) }); - }); - - // Native Clipboard Handlers - ipcMain.on("copy-image-to-clipboard", (event, imageBuffer) => { - try { - const image = nativeImage.createFromBuffer(Buffer.from(imageBuffer)); - clipboard.writeImage(image); - } catch (error) { - console.error("Failed to copy image to clipboard:", error); - } - }); - - ipcMain.on("imgkit-layer-event", (event, payload) => { - event.sender.send("imgkit-layer-event", payload); - }); - - ipcMain.handle("paste-image-from-clipboard", () => { - try { - const image = clipboard.readImage(); - if (!image.isEmpty()) { - return image.toPNG(); - } - return null; - } catch (error) { - console.error("Failed to paste image from clipboard:", error); - return null; - } - }); - - ipcMain.on("FullScreenREQ", (event) => { - mainWindow.setSimpleFullScreen(true); - mainWindow.show(); - }); - - ipcMain.on("DefaultScreenREQ", (event) => { - mainWindow.setSimpleFullScreen(false); - mainWindow.show(); - }); - - ipcMain.on("extensionValueSEND", (event, res) => { - dialog - .showSaveDialog({ - title: "Save Image", - defaultPath: "~/image", - filters: [ - { - name: "Image file", - extensions: [res], - }, - ], - }) - .then((result) => { - event.sender.send("saveAsImgCMD", result.filePath); - }); - }); - - ipcMain.on("saveImgREQ", (event) => { - event.sender.send("saveImgCMD"); - }); - - ipcMain.on("deleteImgREQ", (event) => { - event.sender.send("deleteImgCMD"); - }); - - // Show application menu - ipcMain.on("showMenuREQ", (event) => { - const menu = buildApplicationMenu(event, mainWindow); - Menu.setApplicationMenu(menu); - createPanelView(mainWindow); - }); - - app.on("activate", function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createMainWindow(); - }); }); - // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. -app.on("window-all-closed", function () { - if (process.platform !== "darwin") app.quit(); +electron_1.app.on('window-all-closed', function () { + if (process.platform !== 'darwin') + electron_1.app.quit(); }); - // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. +//# sourceMappingURL=main.js.map \ No newline at end of file diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..f36a8d5 --- /dev/null +++ b/main.ts @@ -0,0 +1,370 @@ +import { + app, + BrowserWindow, + BrowserView, + ipcMain, + dialog, + Menu, + MenuItem, + clipboard, + nativeImage, + IpcMainEvent, + BrowserWindowConstructorOptions, + WebPreferences +} from 'electron'; +import * as path from 'path'; +import { ImageProcessor } from './processing/image_processor'; +import { IPCBridge } from './utils/ipc_bridge'; +import { ContextMenuOptions, MenuItemConfig } from './types'; + +// Import package.json for app metadata +const pkg = require('./package.json'); + +// Electron refresh (only in development) +if (process.env.NODE_ENV === 'development') { + try { + require('electron-reload')(__dirname, { + electron: require(`${__dirname}/node_modules/electron`), + }); + } catch (e) { + console.log('electron-reload not found (this is okay in production)'); + } +} + +// Configuration for browser window creation +const WINDOW_CONFIG: BrowserWindowConstructorOptions = { + width: 1280, + height: 1024, + center: true, + icon: path.join(__dirname, 'assets', 'icon.ico'), + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + } as WebPreferences, +}; + +function createMainWindow(): BrowserWindow { + const mainWindow = new BrowserWindow(WINDOW_CONFIG); + mainWindow.loadFile('index.html'); + return mainWindow; +} + +function createPanelView(mainWindow: BrowserWindow): void { + const view = new BrowserView({ + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + } as WebPreferences, + }); + + view.setBounds({ x: 0, y: 33, width: 1280, height: 90 }); + view.setAutoResize({ width: true, height: false }); + view.webContents.loadFile('./pages/_panel.html'); + mainWindow.addBrowserView(view); +} + +// Context menu helper +function createContextMenu(event: IpcMainEvent, { hasUndo, hasRedo }: ContextMenuOptions): Menu { + const contextMenuItems: MenuItemConfig[] = [ + { label: 'Copy', accelerator: 'Ctrl+C', action: 'copy' }, + { label: 'Paste', accelerator: 'Ctrl+V', action: 'paste' }, + { type: 'separator' }, + { label: 'Delete', accelerator: 'Delete', action: 'delete' }, + { type: 'separator' }, + { label: 'Undo', accelerator: 'Ctrl+Z', action: 'undo', enabled: hasUndo }, + { label: 'Redo', accelerator: 'Ctrl+Y', action: 'redo', enabled: hasRedo }, + { + label: 'Watermark', + accelerator: 'Ctrl+W', + action: 'watermark', + enabled: true, + }, + { type: 'separator' }, + { label: 'Hide', accelerator: 'Alt + H', action: 'hide' }, + ]; + + const menu = new Menu(); + contextMenuItems.forEach((item) => { + if (item.type === 'separator') { + menu.append(new MenuItem({ type: 'separator' })); + } else { + menu.append( + new MenuItem({ + label: item.label!, + accelerator: item.accelerator, + enabled: item.enabled !== false, + click: () => { + event.sender.send('imgkit-context-menu-action', item.action); + }, + }) + ); + } + }); + + return menu; +} + +// Application menu helper +function buildApplicationMenu(event: IpcMainEvent, mainWindow: BrowserWindow): Menu { + const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; + + const IMAGE_EXTENSIONS: string[] = [ + 'pix', + 'png', + 'svg', + 'jpg', + 'jpeg', + 'webp', + 'bmp', + 'gif', + 'ico', + 'tiff', + 'tif', + 'avif', + 'heif', + 'heic', + ]; + + const template: Electron.MenuItemConstructorOptions[] = [ + { + label: 'File', + submenu: [ + { + label: 'Open...', + accelerator: 'Ctrl+O', + click: async () => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openFile', 'multiSelections'], + filters: [{ name: 'Image file', extensions: IMAGE_EXTENSIONS }], + }); + event.sender.send('openImgCMD', result.filePaths); + }, + }, + { + label: 'Save', + accelerator: 'Ctrl+S', + click: () => event.sender.send('saveImgCMD'), + }, + { + label: 'Save As...', + accelerator: 'Ctrl+Shift+S', + click: () => event.sender.send('setExtensionCMD'), + }, + ], + }, + ...(isDev + ? [ + { + label: 'Debug', + submenu: [ + { + label: 'Toggle Developer Tools', + accelerator: 'F12', + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.openDevTools(); + } + }, + }, + { + label: 'Toggle BrowserView Developer Tools', + accelerator: 'Ctrl+Shift+I', + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + const views = mainWindow.getBrowserViews(); + if (views.length > 0) { + views[0].webContents.openDevTools(); + } + } + }, + }, + ], + }, + ] + : []), + { + label: 'Help', + submenu: [ + { + label: 'About', + click: () => { + dialog.showMessageBox({ + title: 'About', + buttons: ['Ok'], + message: `Author : ${pkg.author.name}\nEmail : ${pkg.author.email}\nVersion : v${pkg.version}\nLicense : ${pkg.license}\n`, + }); + }, + }, + ], + }, + ]; + + return Menu.buildFromTemplate(template); +} + +// This method will be called when Electron has finished initialization +app.whenReady().then(() => { + Menu.setApplicationMenu(null); + + const mainWindow: BrowserWindow = createMainWindow(); + + // Initialize IPCBridge instance + const ipcBridge = new IPCBridge(ipcMain, mainWindow); + + // Panel loading handlers - load panel for various image operations + const PANEL_OPERATIONS: string[] = [ + 'resizeImgREQ', + 'cropImgREQ', + 'filterImgREQ', + 'rotateImgREQ', + 'paintImgREQ', + 'image_analysisImgREQ', + ]; + + PANEL_OPERATIONS.forEach((operation) => { + ipcMain.on(operation, () => { + const panelName = operation.replace('ImgREQ', ''); + mainWindow + .getBrowserViews()[0] + .webContents.loadFile(`./pages/${panelName}_panel.html`); + }); + }); + + // IPC Forwarders - Register simple forwarding patterns in bulk + ipcBridge.registerMultiple([ + // Resize commands + { from: 'resizeValueSEND', to: 'resizeImgCMD' }, + { from: 'resizePixelValueSEND', to: 'resizePixelImgCMD' }, + + // Filter commands + { from: 'blurValueSEND', to: 'blurImgCMD' }, + { from: 'sharpenValueSEND', to: 'sharpenImgCMD' }, + { from: 'normalizeImgREQ', to: 'normalizeImgCMD' }, + { from: 'medianValueSEND', to: 'medianImgCMD' }, + { from: 'dilateValueSEND', to: 'dilateImgCMD' }, + { from: 'erodeValueSEND', to: 'erodeImgCMD' }, + { from: 'bitwiseValueSEND', to: 'bitwiseImgCMD' }, + { from: 'negativeImgREQ', to: 'negativeImgCMD' }, + { from: 'grayScaleImgREQ', to: 'grayScaleImgCMD' }, + + // Rotate commands + { from: 'rotateValueSEND', to: 'rotateImgCMD' }, + { from: 'rotateLeftImgREQ', to: 'rotateLeftImgCMD' }, + { from: 'rotateRightImgREQ', to: 'rotateRightImgCMD' }, + { from: 'flipImgREQ', to: 'flipImgCMD' }, + { from: 'flopImgREQ', to: 'flopImgCMD' }, + + // Paint commands + { from: 'tintValueSEND', to: 'tintImgCMD' }, + { from: 'colorpickerImgREQ', to: 'colorpickerImgCMD' }, + { from: 'watermarkUploadREQ', to: 'watermarkUploadCMD' }, + { from: 'watermarkImgREQ', to: 'watermarkImgCMD' }, + { from: 'drawImgREQ', to: 'drawImgCMD' }, + { from: 'padImgREQ', to: 'padImgCMD' }, + + // Crop commands + { from: 'rectCropImgREQ', to: 'cropImgCMD' }, + ]); + + // Custom handler - colorpicker requires broadcast to BrowserViews + ipcMain.on('colorpickerValueSEND', async (event: IpcMainEvent, color: string) => { + const views = mainWindow.getBrowserViews(); + const color_name = await ImageProcessor.getColorName(color); + views.forEach((view) => { + view.webContents.send('colorpickerValueRECV', color, color_name); + }); + }); + + // Notification Handler - Forward to main window + ipcMain.on('showNotificationREQ', (event: IpcMainEvent, notificationId: string) => { + mainWindow.webContents.send('showNotificationCMD', notificationId); + }); + + // ImgKit Context Menu Handler + ipcMain.on('show-imgkit-context-menu', (event: IpcMainEvent, options: ContextMenuOptions) => { + const menu = createContextMenu(event, options); + menu.popup({ window: BrowserWindow.fromWebContents(event.sender)! }); + }); + + // Native Clipboard Handlers + ipcMain.on('copy-image-to-clipboard', (event: IpcMainEvent, imageBuffer: Uint8Array) => { + try { + const image = nativeImage.createFromBuffer(Buffer.from(imageBuffer)); + clipboard.writeImage(image); + } catch (error) { + console.error('Failed to copy image to clipboard:', error); + } + }); + + ipcMain.on('imgkit-layer-event', (event: IpcMainEvent, payload: any) => { + event.sender.send('imgkit-layer-event', payload); + }); + + ipcMain.handle('paste-image-from-clipboard', () => { + try { + const image = clipboard.readImage(); + if (!image.isEmpty()) { + return image.toPNG(); + } + return null; + } catch (error) { + console.error('Failed to paste image from clipboard:', error); + return null; + } + }); + + ipcMain.on('FullScreenREQ', (event: IpcMainEvent) => { + mainWindow.setSimpleFullScreen(true); + mainWindow.show(); + }); + + ipcMain.on('DefaultScreenREQ', (event: IpcMainEvent) => { + mainWindow.setSimpleFullScreen(false); + mainWindow.show(); + }); + + ipcMain.on('extensionValueSEND', async (event: IpcMainEvent, res: string) => { + const result = await dialog.showSaveDialog(mainWindow, { + title: 'Save Image', + defaultPath: '~/image', + filters: [ + { + name: 'Image file', + extensions: [res], + }, + ], + }); + event.sender.send('saveAsImgCMD', result.filePath); + }); + + ipcMain.on('saveImgREQ', (event: IpcMainEvent) => { + event.sender.send('saveImgCMD'); + }); + + ipcMain.on('deleteImgREQ', (event: IpcMainEvent) => { + event.sender.send('deleteImgCMD'); + }); + + // Show application menu + ipcMain.on('showMenuREQ', (event: IpcMainEvent) => { + const menu = buildApplicationMenu(event, mainWindow); + Menu.setApplicationMenu(menu); + createPanelView(mainWindow); + }); + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createMainWindow(); + }); +}); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit(); +}); + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/package-lock.json b/package-lock.json index c4b2c09..5345af0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,10 +18,12 @@ "sharp-ico": "^0.1.5" }, "devDependencies": { + "@types/node": "^25.0.3", "electron": "^37.3.0", "electron-builder": "^26.0.12", "electron-rebuild": "^3.2.9", - "electron-reload": "^2.0.0-alpha.1" + "electron-reload": "^2.0.0-alpha.1", + "typescript": "^5.9.3" } }, "node_modules/@canvas/image-data": { @@ -1333,13 +1335,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/plist": { @@ -2991,6 +2993,23 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/electron/node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5850,9 +5869,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 8fce27c..7fc08e2 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,20 @@ "sharp-ico": "^0.1.5" }, "devDependencies": { + "@types/node": "^25.0.3", "electron": "^37.3.0", "electron-builder": "^26.0.12", "electron-rebuild": "^3.2.9", - "electron-reload": "^2.0.0-alpha.1" + "electron-reload": "^2.0.0-alpha.1", + "typescript": "^5.9.3" }, "scripts": { "postinstall": "electron-builder install-app-deps", + "compile": "bash compile-ts.sh", + "prestart": "npm run compile", "start": "electron .", - "build": "electron-builder", - "build:win-store": "electron-builder --win appx", + "build": "npm run compile && electron-builder", + "build:win-store": "npm run compile && electron-builder --win appx", "rebuild": "electron-rebuild -f -w sharp" }, "build": { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1185bac --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,47 @@ +{ + "compilerOptions": { + /* Language and Environment */ + "target": "ES2020", + "lib": ["ES2020"], + "module": "commonjs", + "moduleResolution": "node", + + /* Emit */ + "outDir": "./", + "rootDir": "./", + "sourceMap": true, + "removeComments": false, + "declaration": false, + "noEmitOnError": false, + + /* Interop Constraints */ + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + + /* Type Checking - Start lenient for gradual migration */ + "allowJs": true, + "checkJs": false, + "strict": false, + "noImplicitAny": false, + "strictNullChecks": false, + "strictFunctionTypes": false, + "strictPropertyInitialization": false, + "noImplicitThis": false, + "alwaysStrict": false, + + /* Completeness */ + "skipLibCheck": true + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "output", + "**/*.spec.ts", + "**/*.test.ts" + ] +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..296ca72 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,330 @@ +/** + * Global Type Definitions for Pegasus Electron App + * This file contains TypeScript type definitions used throughout the application + */ + +import { IpcRenderer } from 'electron'; + +// ============================================================================ +// IPC Channel Names - Type-safe channel definitions +// ============================================================================ + +/** + * IPC channel names for type safety + * Used for communication between main and renderer processes + */ +export declare const IPCChannels: { + readonly RESIZE_IMG_REQ: 'resizeImgREQ'; + readonly CROP_IMG_REQ: 'cropImgREQ'; + readonly FILTER_IMG_REQ: 'filterImgREQ'; + readonly ROTATE_IMG_REQ: 'rotateImgREQ'; + readonly PAINT_IMG_REQ: 'paintImgREQ'; + readonly IMAGE_ANALYSIS_IMG_REQ: 'image_analysisImgREQ'; + readonly RESIZE_IMG_CMD: 'resizeImgCMD'; + readonly RESIZE_PIXEL_IMG_CMD: 'resizePixelImgCMD'; + readonly RESIZE_VALUE_SEND: 'resizeValueSEND'; + readonly RESIZE_PIXEL_VALUE_SEND: 'resizePixelValueSEND'; + readonly BLUR_IMG_CMD: 'blurImgCMD'; + readonly BLUR_VALUE_SEND: 'blurValueSEND'; + readonly SHARPEN_IMG_CMD: 'sharpenImgCMD'; + readonly SHARPEN_VALUE_SEND: 'sharpenValueSEND'; + readonly NORMALIZE_IMG_REQ: 'normalizeImgREQ'; + readonly NORMALIZE_IMG_CMD: 'normalizeImgCMD'; + readonly MEDIAN_IMG_CMD: 'medianImgCMD'; + readonly MEDIAN_VALUE_SEND: 'medianValueSEND'; + readonly DILATE_IMG_CMD: 'dilateImgCMD'; + readonly DILATE_VALUE_SEND: 'dilateValueSEND'; + readonly ERODE_IMG_CMD: 'erodeImgCMD'; + readonly ERODE_VALUE_SEND: 'erodeValueSEND'; + readonly BITWISE_IMG_CMD: 'bitwiseImgCMD'; + readonly BITWISE_VALUE_SEND: 'bitwiseValueSEND'; + readonly NEGATIVE_IMG_REQ: 'negativeImgREQ'; + readonly NEGATIVE_IMG_CMD: 'negativeImgCMD'; + readonly GRAYSCALE_IMG_REQ: 'grayScaleImgREQ'; + readonly GRAYSCALE_IMG_CMD: 'grayScaleImgCMD'; + readonly ROTATE_IMG_CMD: 'rotateImgCMD'; + readonly ROTATE_VALUE_SEND: 'rotateValueSEND'; + readonly ROTATE_LEFT_IMG_REQ: 'rotateLeftImgREQ'; + readonly ROTATE_LEFT_IMG_CMD: 'rotateLeftImgCMD'; + readonly ROTATE_RIGHT_IMG_REQ: 'rotateRightImgREQ'; + readonly ROTATE_RIGHT_IMG_CMD: 'rotateRightImgCMD'; + readonly FLIP_IMG_REQ: 'flipImgREQ'; + readonly FLIP_IMG_CMD: 'flipImgCMD'; + readonly FLOP_IMG_REQ: 'flopImgREQ'; + readonly FLOP_IMG_CMD: 'flopImgCMD'; + readonly TINT_IMG_CMD: 'tintImgCMD'; + readonly TINT_VALUE_SEND: 'tintValueSEND'; + readonly COLORPICKER_IMG_REQ: 'colorpickerImgREQ'; + readonly COLORPICKER_IMG_CMD: 'colorpickerImgCMD'; + readonly COLORPICKER_VALUE_SEND: 'colorpickerValueSEND'; + readonly COLORPICKER_VALUE_RECV: 'colorpickerValueRECV'; + readonly WATERMARK_UPLOAD_REQ: 'watermarkUploadREQ'; + readonly WATERMARK_UPLOAD_CMD: 'watermarkUploadCMD'; + readonly WATERMARK_IMG_REQ: 'watermarkImgREQ'; + readonly WATERMARK_IMG_CMD: 'watermarkImgCMD'; + readonly DRAW_IMG_REQ: 'drawImgREQ'; + readonly DRAW_IMG_CMD: 'drawImgCMD'; + readonly PAD_IMG_REQ: 'padImgREQ'; + readonly PAD_IMG_CMD: 'padImgCMD'; + readonly RECT_CROP_IMG_REQ: 'rectCropImgREQ'; + readonly CROP_IMG_CMD: 'cropImgCMD'; + readonly OPEN_IMG_CMD: 'openImgCMD'; + readonly SAVE_IMG_REQ: 'saveImgREQ'; + readonly SAVE_IMG_CMD: 'saveImgCMD'; + readonly SAVE_AS_IMG_CMD: 'saveAsImgCMD'; + readonly SET_EXTENSION_CMD: 'setExtensionCMD'; + readonly EXTENSION_VALUE_SEND: 'extensionValueSEND'; + readonly DELETE_IMG_REQ: 'deleteImgREQ'; + readonly DELETE_IMG_CMD: 'deleteImgCMD'; + readonly SHOW_MENU_REQ: 'showMenuREQ'; + readonly FULLSCREEN_REQ: 'FullScreenREQ'; + readonly DEFAULT_SCREEN_REQ: 'DefaultScreenREQ'; + readonly SHOW_NOTIFICATION_REQ: 'showNotificationREQ'; + readonly SHOW_NOTIFICATION_CMD: 'showNotificationCMD'; + readonly SHOW_IMGKIT_CONTEXT_MENU: 'show-imgkit-context-menu'; + readonly IMGKIT_CONTEXT_MENU_ACTION: 'imgkit-context-menu-action'; + readonly IMGKIT_LAYER_EVENT: 'imgkit-layer-event'; + readonly COPY_IMAGE_TO_CLIPBOARD: 'copy-image-to-clipboard'; + readonly PASTE_IMAGE_FROM_CLIPBOARD: 'paste-image-from-clipboard'; +}; + +export type IPCChannelName = typeof IPCChannels[keyof typeof IPCChannels]; + +// ============================================================================ +// Image Type Definitions +// ============================================================================ + +/** + * Image metadata information from Sharp + */ +export interface ImageInfo { + format: string; + width: number; + height: number; + channels: number; + space?: string; + depth?: string; + density?: number; + hasProfile?: boolean; + hasAlpha?: boolean; +} + +/** + * Supported image formats + */ +export type ImageFormat = + | 'pix' + | 'png' + | 'svg' + | 'jpg' + | 'jpeg' + | 'webp' + | 'bmp' + | 'gif' + | 'ico' + | 'tiff' + | 'tif' + | 'avif' + | 'heif' + | 'heic'; + +/** + * Image processing options for Sharp pipeline + */ +export interface ImageProcessOptions { + resize?: number; + blur?: number; + sharpen?: number; + normalize?: boolean; + median?: number; + dilate?: number; + erode?: number; + extend?: { + top: number; + bottom: number; + left: number; + right: number; + background: string | { r: number; g: number; b: number; alpha?: number }; + }; + flip?: boolean; + flop?: boolean; + rotate?: number; + tint?: string | { r: number; g: number; b: number }; + negate?: boolean; + grayscale?: boolean; + bitwise?: string; + composite?: Array<{ + input: Buffer; + gravity?: string; + blend?: string; + }>; + extract?: { + left: number; + top: number; + width: number; + height: number; + }; +} + +/** + * Color representation + */ +export interface Color { + r: number; + g: number; + b: number; + alpha?: number; +} + +/** + * Crop coordinates + */ +export interface CropCoordinates { + x: number; + y: number; + width: number; + height: number; +} + +// ============================================================================ +// Layer and Canvas Types +// ============================================================================ + +/** + * Image layer visibility state + */ +export type LayerVisibility = 'visible' | 'hidden'; + +/** + * Layer event payload + */ +export interface LayerEventPayload { + type: string; + layerId: string; + data?: any; +} + +/** + * Context menu options + */ +export interface ContextMenuOptions { + hasUndo: boolean; + hasRedo: boolean; +} + +/** + * Context menu action + */ +export type ContextMenuAction = + | 'copy' + | 'paste' + | 'delete' + | 'undo' + | 'redo' + | 'watermark' + | 'hide'; + +// ============================================================================ +// Mode Types +// ============================================================================ + +/** + * Image editing mode + */ +export type ImageMode = + | 'default' + | 'crop' + | 'draw' + | 'colorpicker' + | 'watermark'; + +/** + * Draw mode state + */ +export interface DrawModeState { + isDrawing: boolean; + startX: number; + startY: number; + currentX: number; + currentY: number; +} + +// ============================================================================ +// GIF Animation Types +// ============================================================================ + +/** + * GIF frame information + */ +export interface GifFrame { + buffer: Buffer; + delay: number; +} + +// ============================================================================ +// Notification Types +// ============================================================================ + +/** + * Notification identifiers + */ +export type NotificationId = + | 'imgkit-name-changed' + | 'imgkit-color-copy' + | 'imgkit-ico-error' + | 'imgkit-open-error' + | 'imgkit-error' + | 'imgkit-convert' + | 'imgkit-save' + | 'imgkit-delete' + | 'imgkit-img-copy' + | 'imgkit-img-paste' + | 'imgkit-copy'; + +// ============================================================================ +// Window Type Extension (for context isolation scenarios) +// ============================================================================ + +/** + * Extended window interface for Electron preload context + * Note: This app uses contextIsolation: false, so this is for future reference + */ +declare global { + interface Window { + electron?: { + ipcRenderer: { + send: (channel: string, ...args: any[]) => void; + on: (channel: string, func: (...args: any[]) => void) => void; + invoke: (channel: string, ...args: any[]) => Promise; + }; + }; + } +} + +// ============================================================================ +// IPC Bridge Types +// ============================================================================ + +/** + * IPC channel mapping for bulk registration + */ +export interface IPCChannelMapping { + from: string; + to: string; + focus?: boolean; +} + +// ============================================================================ +// Menu Types +// ============================================================================ + +/** + * Menu item configuration + */ +export interface MenuItemConfig { + label?: string; + accelerator?: string; + action?: string; + enabled?: boolean; + type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'; +} diff --git a/utils/ipc_bridge.js b/utils/ipc_bridge.js index 9c343a2..76c2ad6 100644 --- a/utils/ipc_bridge.js +++ b/utils/ipc_bridge.js @@ -1,3 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IPCBridge = void 0; /** * IPCBridge - Utility class to simplify IPC communication * @@ -9,108 +12,97 @@ * bridge.registerForwarder('blurValueSEND', 'blurImgCMD'); */ class IPCBridge { - /** - * @param {Electron.IpcMain} ipcMain - Electron IPC Main instance - * @param {Electron.BrowserWindow} targetWindow - Target window to forward messages to - */ - constructor(ipcMain, targetWindow) { - this.ipcMain = ipcMain; - this.targetWindow = targetWindow; - this.registeredChannels = new Set(); - } - - /** - * Register a simple forwarder pattern - * Forwards messages received from fromChannel to toChannel and focuses the window - * - * @param {string} fromChannel - IPC channel name to receive from - * @param {string} toChannel - IPC channel name to send to - * @param {boolean} shouldFocus - Whether to focus window after sending message (default: true) - */ - registerForwarder(fromChannel, toChannel, shouldFocus = true) { - if (this.registeredChannels.has(fromChannel)) { - console.warn( - `[IPCBridge] Channel "${fromChannel}" is already registered. Skipping.` - ); - return; + /** + * @param ipcMain - Electron IPC Main instance + * @param targetWindow - Target window to forward messages to + */ + constructor(ipcMain, targetWindow) { + this.ipcMain = ipcMain; + this.targetWindow = targetWindow; + this.registeredChannels = new Set(); } - - this.ipcMain.on(fromChannel, (event, ...args) => { - this.targetWindow.webContents.send(toChannel, ...args); - if (shouldFocus) { - this.targetWindow.webContents.focus(); - } - }); - - this.registeredChannels.add(fromChannel); - } - - /** - * Register multiple forwarders in bulk - * - * @param {Array<{from: string, to: string, focus?: boolean}>} mappings - Array of channel mappings - * - * @example - * bridge.registerMultiple([ - * { from: 'resizeValueSEND', to: 'resizeImgCMD' }, - * { from: 'blurValueSEND', to: 'blurImgCMD' }, - * { from: 'flipImgREQ', to: 'flipImgCMD' } - * ]); - */ - registerMultiple(mappings) { - mappings.forEach(({ from, to, focus = true }) => { - this.registerForwarder(from, to, focus); - }); - } - - /** - * Register a custom handler (for cases requiring complex logic) - * - * @param {string} channel - IPC channel name to receive from - * @param {Function} handler - Custom handler function - */ - registerCustomHandler(channel, handler) { - if (this.registeredChannels.has(channel)) { - console.warn( - `[IPCBridge] Channel "${channel}" is already registered. Skipping.` - ); - return; + /** + * Register a simple forwarder pattern + * Forwards messages received from fromChannel to toChannel and focuses the window + * + * @param fromChannel - IPC channel name to receive from + * @param toChannel - IPC channel name to send to + * @param shouldFocus - Whether to focus window after sending message (default: true) + */ + registerForwarder(fromChannel, toChannel, shouldFocus = true) { + if (this.registeredChannels.has(fromChannel)) { + console.warn(`[IPCBridge] Channel "${fromChannel}" is already registered. Skipping.`); + return; + } + this.ipcMain.on(fromChannel, (event, ...args) => { + this.targetWindow.webContents.send(toChannel, ...args); + if (shouldFocus) { + this.targetWindow.webContents.focus(); + } + }); + this.registeredChannels.add(fromChannel); } - - this.ipcMain.on(channel, handler); - this.registeredChannels.add(channel); - } - - /** - * Return list of all registered channels - * - * @returns {Array} List of registered channels - */ - getRegisteredChannels() { - return Array.from(this.registeredChannels); - } - - /** - * Unregister a specific channel (mainly for testing) - * - * @param {string} channel - Channel name to unregister - */ - unregister(channel) { - if (this.registeredChannels.has(channel)) { - this.ipcMain.removeAllListeners(channel); - this.registeredChannels.delete(channel); + /** + * Register multiple forwarders in bulk + * + * @param mappings - Array of channel mappings + * + * @example + * bridge.registerMultiple([ + * { from: 'resizeValueSEND', to: 'resizeImgCMD' }, + * { from: 'blurValueSEND', to: 'blurImgCMD' }, + * { from: 'flipImgREQ', to: 'flipImgCMD' } + * ]); + */ + registerMultiple(mappings) { + mappings.forEach(({ from, to, focus = true }) => { + this.registerForwarder(from, to, focus); + }); + } + /** + * Register a custom handler (for cases requiring complex logic) + * + * @param channel - IPC channel name to receive from + * @param handler - Custom handler function + */ + registerCustomHandler(channel, handler) { + if (this.registeredChannels.has(channel)) { + console.warn(`[IPCBridge] Channel "${channel}" is already registered. Skipping.`); + return; + } + this.ipcMain.on(channel, handler); + this.registeredChannels.add(channel); + } + /** + * Return list of all registered channels + * + * @returns List of registered channels + */ + getRegisteredChannels() { + return Array.from(this.registeredChannels); + } + /** + * Unregister a specific channel (mainly for testing) + * + * @param channel - Channel name to unregister + */ + unregister(channel) { + if (this.registeredChannels.has(channel)) { + this.ipcMain.removeAllListeners(channel); + this.registeredChannels.delete(channel); + } + } + /** + * Unregister all registered channels + */ + unregisterAll() { + this.registeredChannels.forEach((channel) => { + this.ipcMain.removeAllListeners(channel); + }); + this.registeredChannels.clear(); } - } - - /** - * Unregister all registered channels - */ - unregisterAll() { - this.registeredChannels.forEach((channel) => { - this.ipcMain.removeAllListeners(channel); - }); - this.registeredChannels.clear(); - } } - +exports.IPCBridge = IPCBridge; +// For backward compatibility with CommonJS require module.exports = { IPCBridge }; +//# sourceMappingURL=ipc_bridge.js.map \ No newline at end of file diff --git a/utils/ipc_bridge.ts b/utils/ipc_bridge.ts new file mode 100644 index 0000000..73e09b1 --- /dev/null +++ b/utils/ipc_bridge.ts @@ -0,0 +1,124 @@ +import { IpcMain, BrowserWindow, IpcMainEvent } from 'electron'; +import { IPCChannelMapping } from '../types'; + +/** + * IPCBridge - Utility class to simplify IPC communication + * + * Eliminates repetitive IPC forwarder patterns and allows declarative channel registration. + * + * @example + * const bridge = new IPCBridge(ipcMain, mainWindow); + * bridge.registerForwarder('resizeValueSEND', 'resizeImgCMD'); + * bridge.registerForwarder('blurValueSEND', 'blurImgCMD'); + */ +export class IPCBridge { + private ipcMain: IpcMain; + private targetWindow: BrowserWindow; + private registeredChannels: Set; + + /** + * @param ipcMain - Electron IPC Main instance + * @param targetWindow - Target window to forward messages to + */ + constructor(ipcMain: IpcMain, targetWindow: BrowserWindow) { + this.ipcMain = ipcMain; + this.targetWindow = targetWindow; + this.registeredChannels = new Set(); + } + + /** + * Register a simple forwarder pattern + * Forwards messages received from fromChannel to toChannel and focuses the window + * + * @param fromChannel - IPC channel name to receive from + * @param toChannel - IPC channel name to send to + * @param shouldFocus - Whether to focus window after sending message (default: true) + */ + registerForwarder(fromChannel: string, toChannel: string, shouldFocus: boolean = true): void { + if (this.registeredChannels.has(fromChannel)) { + console.warn( + `[IPCBridge] Channel "${fromChannel}" is already registered. Skipping.` + ); + return; + } + + this.ipcMain.on(fromChannel, (event: IpcMainEvent, ...args: any[]) => { + this.targetWindow.webContents.send(toChannel, ...args); + if (shouldFocus) { + this.targetWindow.webContents.focus(); + } + }); + + this.registeredChannels.add(fromChannel); + } + + /** + * Register multiple forwarders in bulk + * + * @param mappings - Array of channel mappings + * + * @example + * bridge.registerMultiple([ + * { from: 'resizeValueSEND', to: 'resizeImgCMD' }, + * { from: 'blurValueSEND', to: 'blurImgCMD' }, + * { from: 'flipImgREQ', to: 'flipImgCMD' } + * ]); + */ + registerMultiple(mappings: IPCChannelMapping[]): void { + mappings.forEach(({ from, to, focus = true }) => { + this.registerForwarder(from, to, focus); + }); + } + + /** + * Register a custom handler (for cases requiring complex logic) + * + * @param channel - IPC channel name to receive from + * @param handler - Custom handler function + */ + registerCustomHandler(channel: string, handler: (event: IpcMainEvent, ...args: any[]) => void): void { + if (this.registeredChannels.has(channel)) { + console.warn( + `[IPCBridge] Channel "${channel}" is already registered. Skipping.` + ); + return; + } + + this.ipcMain.on(channel, handler); + this.registeredChannels.add(channel); + } + + /** + * Return list of all registered channels + * + * @returns List of registered channels + */ + getRegisteredChannels(): string[] { + return Array.from(this.registeredChannels); + } + + /** + * Unregister a specific channel (mainly for testing) + * + * @param channel - Channel name to unregister + */ + unregister(channel: string): void { + if (this.registeredChannels.has(channel)) { + this.ipcMain.removeAllListeners(channel); + this.registeredChannels.delete(channel); + } + } + + /** + * Unregister all registered channels + */ + unregisterAll(): void { + this.registeredChannels.forEach((channel) => { + this.ipcMain.removeAllListeners(channel); + }); + this.registeredChannels.clear(); + } +} + +// For backward compatibility with CommonJS require +module.exports = { IPCBridge };