Export/Import project , Deplicate screenshots , Center Button#15
Export/Import project , Deplicate screenshots , Center Button#15BrahimChouih wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds project export/import functionality, screenshot duplication, and a center position reset button to the App Store Screenshot Generator. The changes enable users to share projects as files, duplicate screenshots with all their settings, and quickly reset device positioning to centered defaults. The PR also includes extensive CSS formatting improvements (line breaks and spacing) without changing functionality.
Changes:
- Added export/import functionality for projects as
.screenshotprojectJSON files - Added duplicate screenshot feature that copies all settings (background, device, text, overrides)
- Added center position reset button to quickly return device to default centered position
- Updated project dropdown UI with per-project export buttons and import action
- Applied CSS formatting improvements throughout
styles.cssfor better readability
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| styles.css | Added styles for reset-position-btn, project-export-btn, project-option-info, project-menu-divider, and project-menu-action. Applied formatting improvements to existing animations and selectors. |
| index.html | Added reset-position-btn element in Device tab, added hidden import-project-input file input, updated project menu structure with project-option-info wrapper, and applied HTML formatting improvements. |
| app.js | Implemented exportProjectAsFile(), exportProjectById(), importProjectFromFile(), resetPosition(), and duplicateScreenshot(). Updated updateProjectSelector() to add export buttons per project. Added event handlers for import and reset position features. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Get first image as the primary image (for legacy compatibility) | ||
| let primaryImage = null; | ||
| const firstLang = Object.keys(localizedImages)[0]; | ||
| if (firstLang) { | ||
| primaryImage = localizedImages[firstLang].image; | ||
| } | ||
|
|
||
| // Create screenshot entry | ||
| const screenshot = { | ||
| image: primaryImage, | ||
| name: screenshotData.name || 'Imported Screenshot', | ||
| deviceType: screenshotData.deviceType, | ||
| localizedImages: localizedImages, | ||
| background: screenshotData.background || JSON.parse(JSON.stringify(state.defaults.background)), | ||
| screenshot: screenshotData.screenshot || JSON.parse(JSON.stringify(state.defaults.screenshot)), | ||
| text: screenshotData.text || JSON.parse(JSON.stringify(state.defaults.text)), | ||
| overrides: screenshotData.overrides || {} | ||
| }; | ||
|
|
||
| state.screenshots.push(screenshot); | ||
| } |
There was a problem hiding this comment.
The import function doesn't handle cases where there are no localized images or all image loading fails. If a screenshot has no localized images, primaryImage will be null (line 1712), which could cause issues when rendering. Consider adding a check to ensure at least one valid image exists before adding the screenshot to the state, or provide a fallback/error message for screenshots that fail to load.
| <button class="project-export-btn" data-project-id="${project.id}" title="Export this project"> | ||
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/> | ||
| <polyline points="7 10 12 15 17 10"/> | ||
| <line x1="12" y1="15" x2="12" y2="3"/> | ||
| </svg> | ||
| </button> |
There was a problem hiding this comment.
The project export buttons lack proper ARIA labels for screen readers. While the title attribute provides a tooltip, it's not always read by screen readers. Consider adding aria-label="Export project [project name]" to each export button to improve accessibility for users with screen readers.
| </svg> | ||
| </button> | ||
| <div class="project-menu" id="project-menu"> | ||
| <!-- Projects will be added here dynamically --> | ||
| <!-- Projects and export/import actions are added dynamically by updateProjectSelector() --> |
There was a problem hiding this comment.
The HTML comment references updateProjectSelector() function for dynamic content generation. While this is helpful, consider also mentioning that the export buttons for individual projects are created within this function so developers know where to look when debugging or modifying this feature.
| <!-- Projects and export/import actions are added dynamically by updateProjectSelector() --> | |
| <!-- Projects, per-project export buttons, and export/import actions are added dynamically by updateProjectSelector() --> |
| // Create the new project | ||
| const projectId = 'project_' + Date.now(); | ||
| projects.push({ id: projectId, name: projectName, screenshotCount: 0 }); | ||
| saveProjectsMeta(); | ||
|
|
||
| // Switch to the new project | ||
| currentProjectId = projectId; | ||
|
|
||
| // Reset state | ||
| state.screenshots = []; | ||
| state.selectedIndex = 0; | ||
| state.outputDevice = importData.state.outputDevice || 'iphone-6.9'; | ||
| state.customWidth = importData.state.customWidth || 1290; | ||
| state.customHeight = importData.state.customHeight || 2796; | ||
| state.currentLanguage = importData.state.currentLanguage || 'en'; | ||
| state.projectLanguages = importData.state.projectLanguages || ['en']; | ||
|
|
||
| if (importData.state.defaults) { | ||
| state.defaults = importData.state.defaults; | ||
| } | ||
|
|
||
| // Restore screenshots with their images | ||
| const importedScreenshots = importData.state.screenshots || []; | ||
| for (const screenshotData of importedScreenshots) { | ||
| // Recreate localized images with Image objects | ||
| const localizedImages = {}; | ||
| if (screenshotData.localizedImages) { | ||
| for (const lang of Object.keys(screenshotData.localizedImages)) { | ||
| const langData = screenshotData.localizedImages[lang]; | ||
| if (langData?.src) { | ||
| // Create Image object from base64 | ||
| const img = new Image(); | ||
| await new Promise((resolve, reject) => { | ||
| img.onload = resolve; | ||
| img.onerror = () => reject(new Error(`Failed to load image for ${lang}`)); | ||
| img.src = langData.src; | ||
| }); | ||
|
|
||
| localizedImages[lang] = { | ||
| image: img, | ||
| src: langData.src, | ||
| name: langData.name | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Get first image as the primary image (for legacy compatibility) | ||
| let primaryImage = null; | ||
| const firstLang = Object.keys(localizedImages)[0]; | ||
| if (firstLang) { | ||
| primaryImage = localizedImages[firstLang].image; | ||
| } | ||
|
|
||
| // Create screenshot entry | ||
| const screenshot = { | ||
| image: primaryImage, | ||
| name: screenshotData.name || 'Imported Screenshot', | ||
| deviceType: screenshotData.deviceType, | ||
| localizedImages: localizedImages, | ||
| background: screenshotData.background || JSON.parse(JSON.stringify(state.defaults.background)), | ||
| screenshot: screenshotData.screenshot || JSON.parse(JSON.stringify(state.defaults.screenshot)), | ||
| text: screenshotData.text || JSON.parse(JSON.stringify(state.defaults.text)), | ||
| overrides: screenshotData.overrides || {} | ||
| }; | ||
|
|
||
| state.screenshots.push(screenshot); | ||
| } | ||
|
|
||
| // Save the imported state | ||
| saveState(); |
There was a problem hiding this comment.
The import function switches to a new project and sets currentProjectId before the state is fully loaded and saved. If an error occurs during screenshot loading (lines 1687-1731), the application will be left in an inconsistent state with an empty project. Consider wrapping the entire import process in a transaction-like pattern where the currentProjectId is only updated after all data has been successfully loaded, or implement proper rollback logic in the error handler.
| function duplicateScreenshot(index) { | ||
| if (index < 0 || index >= state.screenshots.length) return; | ||
|
|
||
| const original = state.screenshots[index]; | ||
|
|
||
| // Deep clone the screenshot | ||
| const duplicate = { | ||
| // Copy the image reference (same image object) | ||
| image: original.image, | ||
| name: original.name + ' (copy)', | ||
| deviceType: original.deviceType, | ||
|
|
||
| // Deep clone localized images | ||
| localizedImages: {}, | ||
|
|
||
| // Deep clone settings | ||
| background: JSON.parse(JSON.stringify(original.background)), | ||
| screenshot: JSON.parse(JSON.stringify(original.screenshot)), | ||
| text: JSON.parse(JSON.stringify(original.text)), | ||
| overrides: JSON.parse(JSON.stringify(original.overrides || {})) | ||
| }; | ||
|
|
||
| // Copy localized images (with image object references) | ||
| if (original.localizedImages) { | ||
| Object.keys(original.localizedImages).forEach(lang => { | ||
| const langData = original.localizedImages[lang]; | ||
| if (langData) { | ||
| duplicate.localizedImages[lang] = { | ||
| image: langData.image, // Same image object reference | ||
| src: langData.src, | ||
| name: langData.name | ||
| }; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Insert duplicate after the original | ||
| state.screenshots.splice(index + 1, 0, duplicate); | ||
|
|
||
| // Select the new duplicate | ||
| state.selectedIndex = index + 1; | ||
|
|
||
| // Update UI | ||
| saveState(); | ||
| updateScreenshotList(); | ||
| syncUIWithState(); | ||
| updateGradientStopsUI(); | ||
| updateCanvas(); | ||
| } |
There was a problem hiding this comment.
Duplicate function definition detected. The function duplicateScreenshot is defined twice in this file - once at line 1794 and again here at line 4291. This will cause the second definition to override the first one, which may lead to unexpected behavior. The two implementations differ slightly:
- The first implementation (line 1794) handles file name extensions differently and doesn't call
saveState(). - The second implementation (line 4291) uses a simpler name copy approach and includes
saveState().
Consider either:
- Removing the duplicate definition and keeping only one implementation
- Renaming one of the functions if both are needed for different purposes
- Merging the best parts of both implementations into a single function
| const exportBtn = document.getElementById('export-project-btn'); | ||
| if (exportBtn) exportBtn.classList.add('loading'); | ||
|
|
There was a problem hiding this comment.
The exportBtn element with ID export-project-btn is referenced on line 1485, but this element doesn't exist in the HTML. The export functionality is now triggered through buttons with class project-export-btn in the dropdown menu, which have data-project-id attributes. This will cause exportBtn to be null, and the loading state won't be applied. Either remove these lines or update the selector to match the actual buttons being used.
| const exportBtn = document.getElementById('export-project-btn'); | |
| if (exportBtn) exportBtn.classList.add('loading'); |
| @keyframes spin { | ||
| from { | ||
| transform: rotate(0deg); | ||
| } | ||
|
|
||
| to { | ||
| transform: rotate(360deg); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Duplicate @keyframes spin definition. The same animation is defined at line 1331 and again here at line 2441. This creates redundant code. Consider removing one of the duplicate definitions to maintain cleaner code.
| @keyframes spin { | |
| from { | |
| transform: rotate(0deg); | |
| } | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } |
| // Recreate localized images with Image objects | ||
| const localizedImages = {}; | ||
| if (screenshotData.localizedImages) { | ||
| for (const lang of Object.keys(screenshotData.localizedImages)) { | ||
| const langData = screenshotData.localizedImages[lang]; | ||
| if (langData?.src) { | ||
| // Create Image object from base64 | ||
| const img = new Image(); | ||
| await new Promise((resolve, reject) => { | ||
| img.onload = resolve; | ||
| img.onerror = () => reject(new Error(`Failed to load image for ${lang}`)); | ||
| img.src = langData.src; | ||
| }); | ||
|
|
||
| localizedImages[lang] = { | ||
| image: img, | ||
| src: langData.src, | ||
| name: langData.name | ||
| }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing validation for image data size when importing projects. The import function loads base64 image data from the imported file without checking the size. Maliciously crafted files with extremely large base64 images could cause memory issues or browser crashes. Consider adding validation to check the size of image data before loading, or implementing a maximum file size check for the imported project file itself.
No description provided.