style: format code with Prettier and StandardJS#5
style: format code with Prettier and StandardJS#5deepsource-autofix[bot] wants to merge 12 commits intomainfrom
Conversation
- Added main application script for Conjuration, initializing core components such as UI Manager, Theme Manager, and various tools (Brush Engine, Palette Tool, etc.). - Set up event listeners for window controls, menu management, and tool interactions. - Created a centralized MenuManager class to handle menu interactions and state management. - Implemented canvas size selection dialog with visual previews and resizing functionality. - Added project management features including new, open, and save project functionalities. - Integrated GIF and PNG export capabilities.
- Added PixelCanvas class to handle drawing on a canvas with pixel manipulation. - Implemented methods for drawing pixels, lines, rectangles, ellipses, and flood fill. - Introduced undo/redo functionality with history management. - Added support for various visual effects (grain, static, glitch, CRT, scan lines, vignette, noise, pixelate). - Implemented zooming and grid display features. - Included methods for exporting canvas as PNG and managing pixel data. - Set up event listeners for mouse interactions and cursor position updates.
The [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator can be used to perform null checks before accessing a property, or calling a function.
refactor: convert logical operator to optional chainining
This commit fixes the style issues introduced in 3638d87 according to the output from Prettier and StandardJS. Details: None
Reviewer's GuideReformatted codebase to comply with Prettier and StandardJS conventions, focusing on consistent quotes, indentation, trailing commas, and spacing. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Here's the code health analysis summary for commits Analysis Summary
|
| const data = fs.readFileSync(filePaths[0], "utf8"); | ||
| return { success: true, data: JSON.parse(data), filePath: filePaths[0] }; | ||
| } catch (error) { | ||
| return { success: false, error: error.message }; | ||
| } |
There was a problem hiding this comment.
The use of synchronous file operations (fs.readFileSync in line 142) can block the main thread, leading to performance issues, especially with large files or slow file systems.
Recommendation: Replace synchronous file operations with their asynchronous counterparts (fs.readFile) to improve the application's performance and responsiveness. This change would help prevent the UI from freezing during file operations.
| // Expose app instance to window for Electron IPC | ||
| window.voidApp = { | ||
| hasUnsavedChanges: () => app.hasUnsavedChanges(), | ||
| saveProject: () => app.saveProject(), | ||
| }; |
There was a problem hiding this comment.
Exposing the application instance globally on the window object can lead to security risks, especially if the application is used in a web environment. Malicious scripts can potentially access and manipulate the application state.
Recommendation: Consider using more secure ways to handle IPC or state sharing, such as using more encapsulated, secure postMessage handlers or dedicated secure Electron IPC methods that do not expose direct references to application internals.
| async openProject() { | ||
| try { | ||
| // Check for unsaved changes first | ||
| if (this.state.isModified) { | ||
| const { response } = await this.uiManager.showConfirmDialog( | ||
| "Unsaved Changes", | ||
| "Do you want to save your changes before opening another project?", | ||
| ["Save", "Don't Save", "Cancel"], | ||
| ); | ||
|
|
||
| if (response === 0) { | ||
| // Save | ||
| const saveResult = await this.saveProject(); | ||
| if (!saveResult.success) { | ||
| return; // Don't proceed if save failed | ||
| } | ||
| } else if (response === 2) { | ||
| // Cancel | ||
| return; | ||
| } | ||
|
|
||
| // If response is 1 (Don't Save), proceed with opening | ||
| } | ||
|
|
||
| // Show loading indicator | ||
| const progressToast = this.uiManager.showToast( | ||
| "Opening project...", | ||
| "info", | ||
| 0, | ||
| ); | ||
|
|
||
| // Use Electron IPC to open a file dialog | ||
| const result = await window.voidAPI.openProject(); | ||
|
|
||
| if (result.success && result.data) { | ||
| try { | ||
| // Show saving indicator | ||
| const progressToast = this.uiManager.showToast('Preparing project data...', 'info', 0); | ||
|
|
||
| // Save current frame first | ||
| this.timeline.frames[this.timeline.currentFrameIndex].setImageData( | ||
| this.pixelCanvas.getCanvasImageData() | ||
| ); | ||
|
|
||
| // Prepare project data | ||
| const projectData = this.prepareProjectData(); | ||
|
|
||
| if (!projectData || !projectData.frames || projectData.frames.length === 0) { | ||
| throw new Error('Invalid project data'); | ||
| } | ||
|
|
||
| progressToast.updateMessage('Saving project file...'); | ||
|
|
||
| // Use Electron IPC to save the file | ||
| const result = await window.voidAPI.saveProject(projectData); | ||
|
|
||
| if (result.success) { | ||
| progressToast.close(); | ||
| this.clearModified(); | ||
| this.uiManager.showToast('Project saved successfully', 'success'); | ||
| return { success: true }; | ||
| } else { | ||
| throw new Error(result.error || 'Failed to save project file'); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error saving project:', error); | ||
| this.uiManager.showToast(`Failed to save project: ${error.message}`, 'error', 5000); | ||
|
|
||
| // Log detailed error for debugging | ||
| if (error.stack) { | ||
| console.error('Stack trace:', error.stack); | ||
| // Parse the project data | ||
| const projectData = result.data; | ||
|
|
||
| // Validate project data | ||
| if ( | ||
| !projectData || | ||
| !projectData.frames || | ||
| !Array.isArray(projectData.frames) | ||
| ) { | ||
| throw new Error("Invalid project file format"); | ||
| } | ||
|
|
||
| progressToast.updateMessage("Loading canvas size..."); | ||
|
|
||
| // Load canvas size | ||
| if (projectData.canvasSize) { | ||
| if ( | ||
| !projectData.canvasSize.width || | ||
| !projectData.canvasSize.height || | ||
| projectData.canvasSize.width <= 0 || | ||
| projectData.canvasSize.height <= 0 | ||
| ) { | ||
| throw new Error("Invalid canvas dimensions"); | ||
| } | ||
|
|
||
| return { success: false, error: error.message }; | ||
| } | ||
| } | ||
|
|
||
| async saveProjectAs() { | ||
| try { | ||
| // Show saving indicator | ||
| const progressToast = this.uiManager.showToast('Preparing project data...', 'info', 0); | ||
|
|
||
| // Save current frame first | ||
| this.timeline.frames[this.timeline.currentFrameIndex].setImageData( | ||
| this.pixelCanvas.getCanvasImageData() | ||
|
|
||
| this.pixelCanvas.setCanvasSize( | ||
| projectData.canvasSize.width, | ||
| projectData.canvasSize.height, | ||
| ); | ||
|
|
||
| // Prepare project data | ||
| const projectData = this.prepareProjectData(); | ||
|
|
||
| if (!projectData || !projectData.frames || projectData.frames.length === 0) { | ||
| throw new Error('Invalid project data'); | ||
| } | ||
|
|
||
| progressToast.updateMessage('Saving project file...'); | ||
|
|
||
| // Use Electron IPC to save the file with dialog | ||
| const result = await window.voidAPI.saveProject(projectData); | ||
|
|
||
| if (result.success) { | ||
| // Update project state | ||
| this.state.currentFilePath = result.filePath; | ||
| this.state.projectName = this.getFileNameFromPath(result.filePath); | ||
| this.clearModified(); | ||
| this.updateWindowTitle(); | ||
|
|
||
| progressToast.close(); | ||
| this.uiManager.showToast('Project saved successfully', 'success'); | ||
| return { success: true }; | ||
| } else { | ||
| throw new Error(result.error || 'Failed to save project file'); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error saving project:', error); | ||
| this.uiManager.showToast(`Failed to save project: ${error.message}`, 'error', 5000); | ||
|
|
||
| // Log detailed error for debugging | ||
| if (error.stack) { | ||
| console.error('Stack trace:', error.stack); | ||
| } | ||
|
|
||
| return { success: false, error: error.message }; | ||
| } | ||
| } | ||
|
|
||
| prepareProjectData() { | ||
| // Create a complete project data object | ||
| return { | ||
| appVersion: '0.1.0', | ||
| timestamp: new Date().toISOString(), | ||
| canvasSize: { | ||
| width: this.pixelCanvas.width, | ||
| height: this.pixelCanvas.height | ||
| }, | ||
| frames: this.timeline.getFramesData(), | ||
| palette: this.paletteTool.currentPalette, | ||
| effects: this.effectsEngine.getEffectsSettings(), | ||
| metadata: this.state.metadata, | ||
| loreLayer: this.state.loreLayer | ||
| }; | ||
| } | ||
|
|
||
| getFileNameFromPath(filePath) { | ||
| if (!filePath) return 'Untitled'; | ||
|
|
||
| // Extract file name from path | ||
| const pathParts = filePath.split(/[/\\]/); | ||
| let fileName = pathParts[pathParts.length - 1]; | ||
|
|
||
| // Remove extension | ||
| const extensionIndex = fileName.lastIndexOf('.'); | ||
| if (extensionIndex > 0) { | ||
| fileName = fileName.substring(0, extensionIndex); | ||
| } | ||
|
|
||
| return fileName; | ||
| } | ||
|
|
||
| async exportPNG() { | ||
| try { | ||
| // Show loading indicator | ||
| const progressToast = this.uiManager.showToast('Preparing PNG export...', 'info', 0); | ||
|
|
||
| // Get canvas data URL | ||
| const dataUrl = this.pixelCanvas.getCanvasData(); | ||
|
|
||
| if (!dataUrl || !dataUrl.startsWith('data:image/png;base64,')) { | ||
| throw new Error('Invalid PNG data generated'); | ||
| } | ||
|
|
||
| progressToast.updateMessage('Saving PNG file...'); | ||
|
|
||
| // Use Electron IPC to save the PNG | ||
| const result = await window.voidAPI.exportPng(dataUrl); | ||
|
|
||
| if (result.success) { | ||
| progressToast.close(); | ||
| this.uiManager.showToast('PNG exported successfully', 'success'); | ||
| } else { | ||
| throw new Error(result.error || 'Failed to save PNG file'); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error exporting PNG:', error); | ||
| this.uiManager.showToast(`Failed to export PNG: ${error.message}`, 'error', 5000); | ||
|
|
||
| // Log detailed error for debugging | ||
| if (error.stack) { | ||
| console.error('Stack trace:', error.stack); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async exportGIF() { | ||
| try { | ||
| // Show options dialog for GIF export | ||
| this.uiManager.showModal('Export GIF', this.createGifOptionsForm()); | ||
| } catch (error) { | ||
| console.error('Error showing GIF export dialog:', error); | ||
| this.uiManager.showToast('Failed to open GIF export options: ' + error.message, 'error'); | ||
| } | ||
| } | ||
|
|
||
| createGifOptionsForm() { | ||
| const form = document.createElement('div'); | ||
| form.className = 'gif-options-form'; | ||
|
|
||
| // Loop options | ||
| const loopGroup = document.createElement('div'); | ||
| loopGroup.className = 'form-group'; | ||
|
|
||
| const loopLabel = document.createElement('label'); | ||
| loopLabel.textContent = 'Loop Count:'; | ||
|
|
||
| const loopInput = document.createElement('input'); | ||
| loopInput.type = 'number'; | ||
| loopInput.id = 'gif-loop-count'; | ||
| loopInput.min = '0'; | ||
| loopInput.max = '100'; | ||
| loopInput.value = '0'; | ||
|
|
||
| const loopHelp = document.createElement('small'); | ||
| loopHelp.textContent = '0 = infinite loop'; | ||
|
|
||
| loopGroup.appendChild(loopLabel); | ||
| loopGroup.appendChild(loopInput); | ||
| loopGroup.appendChild(loopHelp); | ||
|
|
||
| // Quality options | ||
| const qualityGroup = document.createElement('div'); | ||
| qualityGroup.className = 'form-group'; | ||
|
|
||
| const qualityLabel = document.createElement('label'); | ||
| qualityLabel.textContent = 'Quality:'; | ||
|
|
||
| const qualitySlider = document.createElement('input'); | ||
| qualitySlider.type = 'range'; | ||
| qualitySlider.id = 'gif-quality'; | ||
| qualitySlider.min = '1'; | ||
| qualitySlider.max = '20'; | ||
| qualitySlider.value = '10'; | ||
|
|
||
| const qualityValue = document.createElement('span'); | ||
| qualityValue.id = 'quality-value'; | ||
| qualityValue.textContent = '10'; | ||
|
|
||
| qualitySlider.addEventListener('input', () => { | ||
| qualityValue.textContent = qualitySlider.value; | ||
| }); | ||
|
|
||
| qualityGroup.appendChild(qualityLabel); | ||
| qualityGroup.appendChild(qualitySlider); | ||
| qualityGroup.appendChild(qualityValue); | ||
|
|
||
| // Dithering option | ||
| const ditherGroup = document.createElement('div'); | ||
| ditherGroup.className = 'form-group'; | ||
|
|
||
| const ditherLabel = document.createElement('label'); | ||
| ditherLabel.textContent = 'Apply Dithering:'; | ||
|
|
||
| const ditherCheckbox = document.createElement('input'); | ||
| ditherCheckbox.type = 'checkbox'; | ||
| ditherCheckbox.id = 'gif-dither'; | ||
|
|
||
| ditherGroup.appendChild(ditherLabel); | ||
| ditherGroup.appendChild(ditherCheckbox); | ||
|
|
||
| // Buttons | ||
| const buttonGroup = document.createElement('div'); | ||
| buttonGroup.className = 'form-buttons'; | ||
|
|
||
| const exportButton = document.createElement('button'); | ||
| exportButton.type = 'button'; | ||
| exportButton.className = 'save-button'; | ||
| exportButton.textContent = 'Export GIF'; | ||
| exportButton.addEventListener('click', () => { | ||
| this.uiManager.closeModal(); | ||
|
|
||
| // Get options | ||
| const options = { | ||
| quality: parseInt(qualitySlider.value), | ||
| dither: ditherCheckbox.checked, | ||
| loop: parseInt(loopInput.value) | ||
| }; | ||
|
|
||
| this.processGifExport(options); | ||
| }); | ||
|
|
||
| const cancelButton = document.createElement('button'); | ||
| cancelButton.type = 'button'; | ||
| cancelButton.className = 'cancel-button'; | ||
| cancelButton.textContent = 'Cancel'; | ||
| cancelButton.addEventListener('click', () => { | ||
| this.uiManager.closeModal(); | ||
| }); | ||
|
|
||
| buttonGroup.appendChild(exportButton); | ||
| buttonGroup.appendChild(cancelButton); | ||
|
|
||
| // Assemble form | ||
| form.appendChild(loopGroup); | ||
| form.appendChild(qualityGroup); | ||
| form.appendChild(ditherGroup); | ||
| form.appendChild(buttonGroup); | ||
|
|
||
| return form; | ||
| } | ||
|
|
||
| async processGifExport(options) { | ||
| try { | ||
| // Show loading indicator with progress | ||
| const progressToast = this.uiManager.showToast('Preparing frames for GIF export...', 'info', 0); | ||
|
|
||
| // Save current frame first to ensure it's included | ||
| this.timeline.frames[this.timeline.currentFrameIndex].setImageData( | ||
| this.pixelCanvas.getCanvasImageData() | ||
| } | ||
|
|
||
| progressToast.updateMessage("Loading frames..."); | ||
|
|
||
| // Load frames | ||
| if (projectData.frames && Array.isArray(projectData.frames)) { | ||
| await this.timeline.setFramesFromData( | ||
| projectData.frames, | ||
| (progress) => { | ||
| progressToast.updateMessage( | ||
| `Loading frames... ${Math.round(progress * 100)}%`, | ||
| ); | ||
| }, | ||
| ); | ||
| // Validate frames | ||
| if (this.timeline.frames.length === 0) { | ||
| throw new Error('No frames to export'); | ||
| } | ||
| // Update progress | ||
| progressToast.updateMessage('Generating GIF...'); | ||
| // Create GIF with options and progress callback | ||
| const gifData = await this.gifExporter.exportGif(options, (progress) => { | ||
| progressToast.updateMessage(`Generating GIF... ${Math.round(progress * 100)}%`); | ||
| }); | ||
|
|
||
| if (!gifData || gifData.length === 0) { | ||
| throw new Error('Failed to generate GIF data'); | ||
| } | ||
| // Update progress | ||
| progressToast.updateMessage('Saving GIF file...'); | ||
|
|
||
| // Use Electron IPC to save the GIF | ||
| const result = await window.voidAPI.exportGif(gifData); | ||
| if (result.success) { | ||
| // Close progress toast | ||
| progressToast.close(); | ||
| this.uiManager.showToast('GIF exported successfully', 'success'); | ||
| } else { | ||
| throw new Error(result.error || 'Failed to save GIF file'); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error exporting GIF:', error); | ||
| this.uiManager.showToast(`Failed to export GIF: ${error.message}`, 'error', 5000); | ||
| // Log detailed error for debugging | ||
| if (error.stack) { | ||
| console.error('Stack trace:', error.stack); | ||
| } | ||
| } | ||
|
|
||
| progressToast.updateMessage("Loading palette..."); | ||
|
|
||
| // Load palette | ||
| if (projectData.palette) { | ||
| this.paletteTool.setPalette(projectData.palette); | ||
| } | ||
|
|
||
| progressToast.updateMessage("Loading effects..."); | ||
|
|
||
| // Load effects | ||
| if (projectData.effects) { | ||
| this.effectsEngine.setEffectsSettings(projectData.effects); | ||
| } | ||
|
|
||
| progressToast.updateMessage("Loading metadata..."); | ||
|
|
||
| // Load metadata | ||
| if (projectData.metadata) { | ||
| this.state.metadata = projectData.metadata; | ||
| } | ||
|
|
||
| // Load lore layer | ||
| if (projectData.loreLayer) { | ||
| this.state.loreLayer = projectData.loreLayer; | ||
| } | ||
|
|
||
| // Update project state | ||
| this.state.currentFilePath = result.filePath; | ||
| this.state.projectName = this.getFileNameFromPath(result.filePath); | ||
| this.clearModified(); | ||
|
|
||
| progressToast.close(); | ||
| this.uiManager.showToast("Project loaded successfully", "success"); | ||
| } catch (parseError) { | ||
| throw new Error( | ||
| `Failed to parse project file: ${parseError.message}`, | ||
| ); | ||
| } | ||
| } else { | ||
| throw new Error(result.error || "No file selected"); | ||
| } | ||
| } catch (error) { | ||
| console.error("Error opening project:", error); | ||
| this.uiManager.showToast( | ||
| `Failed to open project: ${error.message}`, | ||
| "error", | ||
| 5000, | ||
| ); | ||
|
|
||
| // Log detailed error for debugging | ||
| if (error.stack) { | ||
| console.error("Stack trace:", error.stack); | ||
| } | ||
| } |
There was a problem hiding this comment.
The openProject function involves multiple asynchronous operations and interacts with external APIs through IPC. While there is basic error handling in place, the function could benefit from more comprehensive error handling strategies to improve robustness and user experience.
Recommendation: Implement finer-grained error handling and recovery strategies. For instance, ensure that all potential failure points in the asynchronous flow are caught and handled appropriately. Use finally blocks where necessary to clean up resources, like closing any open dialogs or progress indicators, regardless of success or failure.
This commit fixes the style issues introduced in 3638d87 according to the output
from Prettier and StandardJS.
Details: None
Summary by Sourcery
Apply Prettier and StandardJS formatting across the codebase to standardize quotes, indentation, trailing commas, and whitespace in scripts/app.js and main.js.
Chores: